| name | graphql |
| description | GraphQL query language and runtime for APIs enabling clients to request exactly the data they need with strongly-typed schemas and single endpoint architecture. |
| user-invocable | false |
| disable-model-invocation | true |
| progressive_disclosure | {"entry_point":["summary","when_to_use","quick_start"],"full_content":["core_concepts","schema_definition","type_system","queries_mutations","resolvers_dataloaders","subscriptions","error_handling","schema_patterns","server_implementations","client_integrations","typescript_codegen","authentication","performance","file_uploads","testing","production_patterns","framework_integration","comparison","migration","best_practices"]} |
| tokens | {"entry":70,"full":5500} |
GraphQL Skill
Summary
GraphQL is a query language and runtime for APIs that enables clients to request exactly the data they need. It provides a strongly-typed schema, single endpoint architecture, and eliminates over-fetching/under-fetching problems common in REST APIs.
When to Use
- Building flexible APIs for multiple client types (web, mobile, IoT)
- Complex data requirements with nested relationships
- Mobile-first applications needing bandwidth efficiency
- Reducing API versioning complexity
- Real-time data with subscriptions
- Microservices aggregation and federation
- Developer experience with strong typing and introspection
Quick Start
1. Define Schema (SDL)
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
publishedAt: DateTime
}
type Query {
user(id: ID!): User
users: [User!]!
post(id: ID!): Post
}
createPost String, String, ID Post
updatePost ID, String, String Post
deletePost ID Boolean
2. Write Resolvers (TypeScript + Apollo Server)
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { readFileSync } from 'fs';
const typeDefs = readFileSync('./schema.graphql', 'utf-8');
const users = [
{ id: '1', name: 'Alice', email: 'alice@example.com' },
{ id: '2', name: 'Bob', email: 'bob@example.com' },
];
const posts = [
{ id: '1', title: 'GraphQL Intro', content: 'Learning GraphQL...', authorId: '1' },
{ id: '2', title: 'Apollo Server', content: 'Building APIs...', authorId: '1' },
];
const resolvers = {
Query: {
user: (_, { id }) => users.find( u. === id),
: users,
: posts.( p. === id),
},
: {
: {
post = {
: (posts. + ),
title,
content,
authorId,
};
posts.(post);
post;
},
: {
post = posts.( p. === id);
(!post) ();
(title) post. = title;
(content) post. = content;
post;
},
: {
index = posts.( p. === id);
(index === -) ;
posts.(index, );
;
},
},
: {
: posts.( p. === user.),
},
: {
: users.( u. === post.),
},
};
server = ({ typeDefs, resolvers });
(server, {
: { : },
}).( {
.();
});
3. Query Data (Client)
import { ApolloClient, InMemoryCache, gql } from '@apollo/client';
const client = new ApolloClient({
uri: 'http://localhost:4000',
cache: new InMemoryCache(),
});
const GET_USER = gql`
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
posts {
id
title
publishedAt
}
}
}
`;
const { data } = await client.query({
query: GET_USER,
variables: { id: '1' },
});
const CREATE_POST = gql`
mutation CreatePost($title: String!, $content: String!, $authorId: ID!
createPost , , )
id
title
content
`;
{ : postData } = client.({
: ,
: {
: ,
: ,
: ,
},
});
Core Concepts
GraphQL Fundamentals
- Schema-First Design: Define API contract with Schema Definition Language (SDL)
- Type Safety: Strongly-typed schema enforced at runtime and build-time
- Single Endpoint: All queries and mutations go through one URL (e.g.,
/graphql)
- Client-Specified Queries: Clients request exactly what they need
- Hierarchical Data: Queries mirror the shape of returned data
- Introspection: Schema is self-documenting and queryable
Operations
query GetUser {
user(id: "1") {
name
}
}
mutation CreateUser {
createUser(name: "Alice", email: "alice@example.com") {
id
name
}
}
subscription OnPostCreated {
postCreated {
id
title
author {
name
}
}
}
Fields and Arguments
type Query {
user(id: ID!): User
users(limit: Int = 10, offset: Int = 0): [User!]!
searchPosts(
query: String!
category: String
limit: Int = 20
): [Post!]!
}
Schema Definition Language (SDL)
Basic Type Definition
type User {
id: ID!
name: String!
email: String!
age: Int
isActive: Boolean!
posts: [Post!]!
profile: Profile
}
type Profile {
bio: String
avatarUrl: String
website: String
}
type Post {
id: ID!
title: String!
content: String!
author: User!
tags: String
DateTime
Input Types (for mutations)
input CreateUserInput {
name: String!
email: String!
age: Int
}
input UpdateUserInput {
name: String
email: String
age: Int
}
type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User!
}
Interfaces
interface Node {
id: ID!
createdAt: DateTime!
updatedAt: DateTime!
}
type User implements Node {
id: ID!
createdAt: DateTime!
updatedAt: DateTime!
name: String!
email: String!
}
type Post implements Node {
id: ID!
createdAt: DateTime!
updatedAt: DateTime!
title: String!
content: String!
}
type Query {
node(id: ID!): Node
Unions
union SearchResult = User | Post | Comment
type Query {
search(query: String!): [SearchResult!]!
}
query Search {
search(query: "graphql") {
... on User {
name
email
}
... on Post {
title
content
}
... on Comment {
text
author { name }
}
}
}
Enums
enum Role {
ADMIN
MODERATOR
USER
GUEST
}
enum PostStatus {
DRAFT
PUBLISHED
ARCHIVED
}
type User {
id: ID!
name: String!
role: Role!
}
type Post {
id: ID!
title: String!
status: PostStatus!
}
Type System
Scalar Types
scalar Int
scalar Float
scalar String
scalar Boolean
scalar ID
scalar DateTime
scalar Email
scalar URL
scalar JSON
scalar Upload
Custom Scalar Implementation
import { GraphQLScalarType, Kind } from 'graphql';
const DateTimeScalar = new GraphQLScalarType({
name: 'DateTime',
description: 'ISO 8601 DateTime',
serialize(value: Date) {
return value.toISOString();
},
parseValue(value: string) {
return new Date(value);
},
parseLiteral(ast) {
if (ast.kind === Kind.STRING) {
return new Date(ast.value);
}
return null;
},
});
const resolvers = {
DateTime: DateTimeScalar,
};
Non-Null and Lists
type User {
name: String!
email: String
tags: [String!]!
friends: [User!]
posts: [Post]!
comments: [Comment]
}
Queries and Mutations
Query Variables
const GET_USER = gql`
query GetUser($id: ID!, $includePosts: Boolean = false) {
user(id: $id) {
id
name
email
posts @include(if: $includePosts) {
id
title
}
}
}
`;
const { data } = await client.query({
query: GET_USER,
variables: {
id: '1',
includePosts: true,
},
});
Aliases
query {
user1: user(id: "1") {
name
}
user2: user(id: "2") {
name
}
currentUser: me {
id
name
}
}
Fragments
fragment UserFields on User {
id
name
email
createdAt
}
fragment PostSummary on Post {
id
title
publishedAt
author {
...UserFields
}
}
query {
user(id: "1") {
...UserFields
posts {
...PostSummary
}
}
}
Directives
query GetUser($id: ID!, $withPosts: Boolean!, $skipEmail: Boolean!) {
user(id: $id) {
name
email @skip(if: $skipEmail)
posts @include(if: $withPosts) {
title
}
}
}
directive @auth(requires: Role = USER) on FIELD_DEFINITION
type Query {
users: [User!]! @auth(requires: ADMIN)
me: User! @auth
Mutations Best Practices
type Mutation {
createPost(input: CreatePostInput!): CreatePostPayload!
updatePost(id: ID!, input: UpdatePostInput!): UpdatePostPayload!
deletePost(id: ID!): DeletePostPayload!
}
input CreatePostInput {
title: String!
content: String!
categoryId: ID!
tags: [String!]
}
type CreatePostPayload {
post: Post
userErrors: [UserError
Boolean
UserError
String
String
Resolvers and DataLoaders
Resolver Signature
type Resolver<TParent, TArgs, TContext, TResult> = (
parent: TParent,
args: TArgs,
context: TContext,
info: GraphQLResolveInfo
) => TResult | Promise<TResult>;
Basic Resolvers
const resolvers = {
Query: {
user: async (_, { id }, { db }) => {
return db.users.findById(id);
},
users: async (_, { limit = 10, offset = 0 }, { db }) => {
return db.users.findMany({ limit, offset });
},
},
Mutation: {
createUser: async (_, { input }, { db, userId }) => {
if (!userId) {
throw new Error('Authentication required');
}
const user = await db.users.create(input);
return { user, userErrors: [], success: true };
},
},
User: {
posts: async (user, _, { db }) => {
return db.posts.findByAuthorId(user.id);
},
fullName: (user) => {
return `${user.firstName} ${user.lastName}`;
},
},
};
The N+1 Problem
const resolvers = {
Query: {
users: () => db.users.findMany(),
},
User: {
posts: (user) => db.posts.findByAuthorId(user.id),
},
};
DataLoader Solution
import DataLoader from 'dataloader';
async function batchLoadPosts(authorIds: string[]) {
const posts = await db.posts.findByAuthorIds(authorIds);
const postsByAuthor = authorIds.map(authorId =>
posts.filter(post => post.authorId === authorId)
);
return postsByAuthor;
}
function createContext({ req }) {
return {
db,
userId: req.userId,
loaders: {
posts: new DataLoader(batchLoadPosts),
},
};
}
const resolvers = {
Query: {
users: () => db.users.findMany(),
},
User: {
posts: (user, _, { loaders }) => {
loaders..(user.);
},
},
};
Advanced DataLoader Patterns
const userLoader = new DataLoader(
async (ids) => {
const users = await db.users.findByIds(ids);
return ids.map(id => users.find(u => u.id === id));
},
{
cache: true,
maxBatchSize: 100,
batchScheduleFn: (cb) => setTimeout(cb, 10),
}
);
userLoader.clear(id);
userLoader.clearAll();
userLoader.prime(id, user);
Subscriptions
WebSocket Setup (Apollo Server)
import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@apollo/server/express4';
import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer';
import { createServer } from 'http';
import { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import { makeExecutableSchema } from '@graphql-tools/schema';
import express from 'express';
const app = express();
const httpServer = createServer(app);
const schema = makeExecutableSchema({ typeDefs, resolvers });
const wsServer = new WebSocketServer({
server: httpServer,
path: '/graphql',
});
const serverCleanup = useServer({ schema }, wsServer);
const server = new ApolloServer({
schema,
plugins: [
ApolloServerPluginDrainHttpServer({ httpServer }),
{
() {
{
() {
serverCleanup.();
},
};
},
},
],
});
server.();
app.(, express.(), (server));
httpServer.();
Subscription Schema
type Subscription {
postCreated: Post!
postUpdated(id: ID!): Post!
messageAdded(channelId: ID!): Message!
userStatusChanged(userId: ID!): UserStatus!
}
type Message {
id: ID!
text: String!
author: User!
channelId: ID!
createdAt: DateTime!
}
enum UserStatus {
ONLINE
OFFLINE
AWAY
}
Subscription Resolvers (PubSub)
import { PubSub } from 'graphql-subscriptions';
const pubsub = new PubSub();
const resolvers = {
Mutation: {
createPost: async (_, { input }, { db }) => {
const post = await db.posts.create(input);
pubsub.publish('POST_CREATED', { postCreated: post });
return { post, success: true, userErrors: [] };
},
sendMessage: async (_, { channelId, text }, { db, userId }) => {
const message = await db.messages.create({
channelId,
text,
authorId: userId,
});
pubsub.publish(`MESSAGE_${channelId}`, {
messageAdded: message,
});
return message;
},
},
Subscription: {
postCreated: {
subscribe: () => pubsub.asyncIterator(['POST_CREATED']),
},
postUpdated: {
subscribe: (_, { id }) => pubsub.asyncIterator([`POST_UPDATED_`]),
},
: {
: {
pubsub.([]);
},
},
},
};
Client Subscriptions (Apollo Client)
import { ApolloClient, InMemoryCache, split, HttpLink } from '@apollo/client';
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { getMainDefinition } from '@apollo/client/utilities';
import { createClient } from 'graphql-ws';
const httpLink = new HttpLink({
uri: 'http://localhost:4000/graphql',
});
const wsLink = new GraphQLWsLink(
createClient({
url: 'ws://localhost:4000/graphql',
})
);
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
wsLink,
httpLink
);
const client = new ApolloClient({
link: splitLink,
cache: new InMemoryCache(),
});
= gql`;
() {
{ data, loading } = (, {
: { channelId },
});
(loading) ;
;
}
Redis PubSub (Production)
import { RedisPubSub } from 'graphql-redis-subscriptions';
import Redis from 'ioredis';
const options = {
host: 'localhost',
port: 6379,
retryStrategy: (times) => Math.min(times * 50, 2000),
};
const pubsub = new RedisPubSub({
publisher: new Redis(options),
subscriber: new Redis(options),
});
pubsub.publish('POST_CREATED', { postCreated: post });
pubsub.asyncIterator(['POST_CREATED']);
Error Handling
Error Types
import { GraphQLError } from 'graphql';
class AuthenticationError extends GraphQLError {
constructor(message: string) {
super(message, {
extensions: {
code: 'UNAUTHENTICATED',
http: { status: 401 },
},
});
}
}
class ForbiddenError extends GraphQLError {
constructor(message: string) {
super(message, {
extensions: {
code: 'FORBIDDEN',
http: { status: 403 },
},
});
}
}
class ValidationError extends GraphQLError {
constructor(message: string, invalidFields: Record<string, string>) {
super(message, {
extensions: {
code: 'BAD_USER_INPUT',
invalidFields,
},
});
}
}
Throwing Errors in Resolvers
const resolvers = {
Query: {
user: async (_, { id }, { db, userId }) => {
if (!userId) {
throw new AuthenticationError('Must be logged in');
}
const user = await db.users.findById(id);
if (!user) {
throw new GraphQLError('User not found', {
extensions: { code: 'NOT_FOUND' },
});
}
return user;
},
},
Mutation: {
createPost: async (_, { input }, { db, userId }) => {
const errors: Record<string, string> = {};
if (!input.title || input.title.length < 3) {
errors.title = 'Title must be at least 3 characters';
}
if (!input.content) {
errors.content = 'Content is required';
}
if (Object.keys(errors).length > 0) {
throw new ValidationError(, errors);
}
post = db..({ ...input, : userId });
{ post, : , : [] };
},
},
};
Error Response Format
{
"errors": [
{
"message": "User not found",
"locations": [{ "line": 2, "column": 3 }],
"path": ["user"],
"extensions": {
"code": "NOT_FOUND"
}
}
],
"data": {
"user": null
}
}
Field-Level Error Handling
type Query {
user(id: ID!): User # null on error
users: [User!]! # throws on error
post(id: ID!): Post # null on error
}
const resolvers = {
Query: {
user: async (_, { id }, { db }) => {
try {
return await db.users.findById(id);
} catch (error) {
console.error('Failed to fetch user:', error);
return null;
}
},
},
};
Schema Design Patterns
Relay Cursor Connections
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
type PostEdge {
node: Post!
cursor: String!
}
type PostConnection {
edges: [PostEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type Query {
posts(
first: Int
after: String
last: Int
before: String
): PostConnection!
}
Relay Connection Resolver
import { fromGlobalId, toGlobalId } from 'graphql-relay';
function encodeCursor(id: string): string {
return Buffer.from(`cursor:${id}`).toString('base64');
}
function decodeCursor(cursor: string): string {
return Buffer.from(cursor, 'base64').toString('utf-8').replace('cursor:', '');
}
const resolvers = {
Query: {
posts: async (_, { first = 10, after }, { db }) => {
const startId = after ? decodeCursor(after) : null;
const posts = await db.posts.findMany({
where: startId ? { id: { gt: startId } } : {},
take: first + 1,
orderBy: { createdAt: 'desc' },
});
hasNextPage = posts. > first;
nodes = hasNextPage ? posts.(, -) : posts;
edges = nodes.( ({
node,
: (node.),
}));
{
edges,
: {
hasNextPage,
: !!after,
: edges[]?.,
: edges[edges. - ]?.,
},
: db..(),
};
},
},
};
Offset Pagination (Simpler)
type PostsResponse {
posts: [Post!]!
total: Int!
hasMore: Boolean!
}
type Query {
posts(limit: Int = 10, offset: Int = 0): PostsResponse!
}
Global Object Identification (Relay)
interface Node {
id: ID!
}
type User implements Node {
id: ID!
name: String!
}
type Post implements Node {
id: ID!
title: String!
}
type Query {
node(id: ID!): Node
}
const resolvers = {
Query: {
node: async (_, { id }, { db }) => {
const { type, id: rawId } = fromGlobalId(id);
switch (type) {
case 'User':
return db.users.findById(rawId);
case 'Post':
return db.posts.findById(rawId);
default:
return null;
}
},
},
User: {
id: (user) => toGlobalId('User', user.id),
},
Post: {
id: (post) => toGlobalId('Post', post.id),
},
};
Server Implementations
Apollo Server (TypeScript)
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
const server = new ApolloServer({
typeDefs,
resolvers,
introspection: process.env.NODE_ENV !== 'production',
plugins: [
{
async requestDidStart() {
return {
async willSendResponse({ response }) {
console.log('Response:', response);
},
};
},
},
],
});
const { url } = await startStandaloneServer(server, {
listen: { port: 4000 },
context: async ({ req }) => ({
token: req.headers.authorization,
db: database,
}),
});
GraphQL Yoga (Modern Alternative)
import { createYoga, createSchema } from 'graphql-yoga';
import { createServer } from 'node:http';
const yoga = createYoga({
schema: createSchema({
typeDefs,
resolvers,
}),
graphiql: true,
context: ({ request }) => ({
userId: request.headers.get('x-user-id'),
}),
});
const server = createServer(yoga);
server.listen(4000, () => {
console.log('Server on http://localhost:4000/graphql');
});
Graphene (Python/Django)
import graphene
from graphene_django import DjangoObjectType
from .models import User, Post
class UserType(DjangoObjectType):
class Meta:
model = User
fields = '__all__'
class PostType(DjangoObjectType):
class Meta:
model = Post
fields = '__all__'
class Query(graphene.ObjectType):
users = graphene.List(UserType)
user = graphene.Field(UserType, id=graphene.ID(required=True))
posts = graphene.List(PostType)
def resolve_users(self, info):
return User.objects.all()
def resolve_user(self, info, id):
return User.objects.get(pk=id)
def resolve_posts(self, info):
return Post.objects.all()
class CreateUser(graphene.Mutation):
class Arguments:
name = graphene.String(required=True)
email = graphene.String(required=True)
user = graphene.Field(UserType)
success = graphene.Boolean()
():
user = User.objects.create(name=name, email=email)
CreateUser(user=user, success=)
(graphene.ObjectType):
create_user = CreateUser.Field()
schema = graphene.Schema(query=Query, mutation=Mutation)
Strawberry (Python, Modern)
import strawberry
from typing import List, Optional
from datetime import datetime
@strawberry.type
class User:
id: strawberry.ID
name: str
email: str
created_at: datetime
@strawberry.type
class Post:
id: strawberry.ID
title: str
content: str
author_id: strawberry.ID
@strawberry.input
class CreateUserInput:
name: str
email: str
@strawberry.type
class Query:
@strawberry.field
def users(self) -> List[User]:
return User.objects.all()
@strawberry.field
def user(self, id: strawberry.ID) -> Optional[User]:
return User.objects.get(pk=id)
@strawberry.type
class Mutation:
@strawberry.mutation
def create_user(self, input: CreateUserInput) -> User:
User.objects.create(**.__dict__)
schema = strawberry.Schema(query=Query, mutation=Mutation)
strawberry.fastapi GraphQLRouter
app = FastAPI()
app.include_router(GraphQLRouter(schema), prefix=)
Client Integrations
Apollo Client (React)
import { ApolloClient, InMemoryCache, ApolloProvider, useQuery, useMutation } from '@apollo/client';
const client = new ApolloClient({
uri: 'http://localhost:4000/graphql',
cache: new InMemoryCache(),
});
function App() {
return (
<ApolloProvider client={client}>
<UserList />
</ApolloProvider>
);
}
function UserList() {
const { loading, error, data, refetch } = useQuery(GET_USERS);
const [createUser] = useMutation(CREATE_USER, {
refetchQueries: [{ query: GET_USERS }],
});
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
);
}
urql (Lightweight Alternative)
import { createClient, Provider, useQuery, useMutation } from 'urql';
const client = createClient({
url: 'http://localhost:4000/graphql',
});
function App() {
return (
<Provider value={client}>
<UserList />
</Provider>
);
}
function UserList() {
const [result, reexecuteQuery] = useQuery({ query: GET_USERS });
const [, createUser] = useMutation(CREATE_USER);
const { data, fetching, error } = result;
if (fetching) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<div>
{data.users.map(user => (
<div key={user.id}>{user.name}</div>
))}
);
}
graphql-request (Minimal)
import { GraphQLClient, gql } from 'graphql-request';
const client = new GraphQLClient('http://localhost:4000/graphql');
async function fetchUsers() {
const query = gql`
query {
users {
id
name
}
}
`;
const data = await client.request(query);
return data.users;
}
async function createUser(name: string) {
const mutation = gql`
mutation CreateUser($name: String!) {
createUser(name: $name) {
id
name
}
}
`;
const data = await client.request(mutation, { name });
return data.createUser;
}
TanStack Query + GraphQL
import { useQuery, useMutation, QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { GraphQLClient } from 'graphql-request';
const graphQLClient = new GraphQLClient('http://localhost:4000/graphql');
function useUsers() {
return useQuery({
queryKey: ['users'],
queryFn: async () => {
const { users } = await graphQLClient.request(GET_USERS);
return users;
},
});
}
function useCreateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (name: string) => {
const { createUser } = await graphQLClient.request(CREATE_USER, { name });
return createUser;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
},
});
}
TypeScript Code Generation
GraphQL Code Generator Setup
npm install -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typescript-react-apollo
codegen.yml
schema: http://localhost:4000/graphql
documents: 'src/**/*.graphql'
generates:
src/generated/graphql.ts:
plugins:
- typescript
- typescript-operations
- typescript-react-apollo
config:
withHooks: true
withComponent: false
skipTypename: false
enumsAsTypes: true
Generated Types
export type GetUsersQuery = {
__typename?: 'Query';
users: Array<{
__typename?: 'User';
id: string;
name: string;
email: string;
}>;
};
export function useGetUsersQuery(
baseOptions?: Apollo.QueryHookOptions<GetUsersQuery, GetUsersQueryVariables>
) {
return Apollo.useQuery<GetUsersQuery, GetUsersQueryVariables>(
GetUsersDocument,
baseOptions
);
}
Usage with Generated Types
import { useGetUsersQuery, useCreateUserMutation } from './generated/graphql';
function UserList() {
const { data, loading, error } = useGetUsersQuery();
const [createUser] = useCreateUserMutation();
const users = data?.users;
}
Authentication and Authorization
Context-Based Auth
import jwt from 'jsonwebtoken';
async function createContext({ req }) {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return { userId: null, db };
}
try {
const { userId } = jwt.verify(token, process.env.JWT_SECRET);
return { userId, db };
} catch (error) {
return { userId: null, db };
}
}
const server = new ApolloServer({
typeDefs,
resolvers,
});
await startStandaloneServer(server, {
context: createContext,
});
Resolver-Level Auth
const resolvers = {
Query: {
me: (_, __, { userId }) => {
if (!userId) {
throw new AuthenticationError('Not authenticated');
}
return db.users.findById(userId);
},
users: (_, __, { userId, db }) => {
if (!userId) {
throw new AuthenticationError('Not authenticated');
}
const user = db.users.findById(userId);
if (user.role !== 'ADMIN') {
throw new ForbiddenError('Admin access required');
}
return db.users.findMany();
},
},
};
Directive-Based Auth
import { mapSchema, getDirective, MapperKind } from '@graphql-tools/utils';
const typeDefs = gql`
directive @auth(requires: Role = USER) on FIELD_DEFINITION
enum Role {
ADMIN
USER
}
type Query {
users: [User!]! @auth(requires: ADMIN)
me: User! @auth
}
`;
function authDirectiveTransformer(schema, directiveName = 'auth') {
return mapSchema(schema, {
[MapperKind.OBJECT_FIELD]: (fieldConfig) => {
const authDirective = getDirective(schema, fieldConfig, directiveName)?.[0];
if (authDirective) {
const { requires } = authDirective;
{ resolve = defaultFieldResolver } = fieldConfig;
fieldConfig. = (source, args, context, info) => {
(!context.) {
();
}
(requires) {
user = context...(context.);
(user. !== requires) {
();
}
}
(source, args, context, info);
};
}
fieldConfig;
},
});
}
schema = ({ typeDefs, resolvers });
schema = (schema);
Performance Optimization
Query Complexity Analysis
import { createComplexityLimitRule } from 'graphql-validation-complexity';
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
createComplexityLimitRule(1000, {
scalarCost: 1,
objectCost: 2,
listFactor: 10,
}),
],
});
Persistent Queries (APQ)
import { ApolloServer } from '@apollo/server';
import { ApolloServerPluginInlineTraceDisabled } from '@apollo/server/plugin/disabled';
const server = new ApolloServer({
typeDefs,
resolvers,
persistedQueries: {
cache: new Map(),
},
});
Response Caching
import responseCachePlugin from '@apollo/server-plugin-response-cache';
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [
responseCachePlugin({
sessionId: (context) => context.userId || null,
shouldReadFromCache: (context) => !context.userId,
}),
],
});
const typeDefs = gql`
type Query {
posts: [Post!]! @cacheControl(maxAge: 60)
user(id: ID!): User @cacheControl(maxAge: 30)
}
`;
Field-Level Caching
import { InMemoryLRUCache } from '@apollo/utils.keyvaluecache';
const cache = new InMemoryLRUCache({
maxSize: Math.pow(2, 20) * 100,
ttl: 300,
});
const resolvers = {
Query: {
user: async (_, { id }, { cache, db }) => {
const cacheKey = `user:${id}`;
const cached = await cache.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
const user = await db.users.findById(id);
await cache.set(cacheKey, JSON.stringify(user), { ttl: 60 });
return user;
},
},
};
File Uploads
Schema
scalar Upload
type Mutation {
uploadFile(file: Upload!): File!
uploadMultiple(files: [Upload!]!): [File!]!
}
type File {
filename: String!
mimetype: String!
encoding: String!
url: String!
}
Server (graphql-upload)
import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs';
import { GraphQLUpload } from 'graphql-upload/GraphQLUpload.mjs';
import fs from 'fs';
import path from 'path';
app.use('/graphql', graphqlUploadExpress({ maxFileSize: 10000000, maxFiles: 10 }));
const resolvers = {
Upload: GraphQLUpload,
Mutation: {
uploadFile: async (_, { file }) => {
const { createReadStream, filename, mimetype, encoding } = await file;
const stream = createReadStream();
const uploadPath = path.join(__dirname, 'uploads', filename);
await new Promise((resolve, reject) => {
stream
.pipe(fs.createWriteStream(uploadPath))
.on('finish', resolve)
.on('error', reject);
});
return {
filename,
mimetype,
encoding,
url: `/uploads/`,
};
},
},
};
Client (Apollo Client)
import { useMutation } from '@apollo/client';
const UPLOAD_FILE = gql`
mutation UploadFile($file: Upload!) {
uploadFile(file: $file) {
filename
url
}
}
`;
function FileUpload() {
const [uploadFile] = useMutation(UPLOAD_FILE);
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
uploadFile({ variables: { file } });
};
return <input type="file" onChange={handleFileChange} />;
}
Testing
Unit Testing Resolvers
import { describe, it, expect, vi } from 'vitest';
describe('User Resolvers', () => {
it('should fetch user by ID', async () => {
const mockDb = {
users: {
findById: vi.fn().mockResolvedValue({
id: '1',
name: 'Alice',
email: 'alice@example.com',
}),
},
};
const result = await resolvers.Query.user(
null,
{ id: '1' },
{ db: mockDb, userId: '1' },
{} as any
);
expect(result.name).toBe('Alice');
expect(mockDb.users.findById).toHaveBeenCalledWith('1');
});
it('should throw error when not authenticated', async () => {
await expect(
resolvers.Query.me(null, {}, { userId: }, {} )
)..();
});
});
Integration Testing (Apollo Server)
import { ApolloServer } from '@apollo/server';
import assert from 'assert';
it('fetches users', async () => {
const server = new ApolloServer({ typeDefs, resolvers });
const response = await server.executeOperation({
query: 'query { users { id name } }',
});
assert(response.body.kind === 'single');
expect(response.body.singleResult.errors).toBeUndefined();
expect(response.body.singleResult.data?.users).toHaveLength(2);
});
it('creates user', async () => {
const response = await server.executeOperation({
query: `
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
user { id name }
success
}
}
`,
variables: {
input: { name: 'Charlie', email: 'charlie@example.com' },
},
});
(response.. === );
(response...?..).();
});
E2E Testing (Supertest)
import request from 'supertest';
import { app } from './server';
describe('GraphQL API', () => {
it('should query users', async () => {
const response = await request(app)
.post('/graphql')
.send({
query: '{ users { id name } }',
})
.expect(200);
expect(response.body.data.users).toBeDefined();
});
it('should require authentication', async () => {
const response = await request(app)
.post('/graphql')
.send({
query: '{ me { id } }',
})
.expect(200);
expect(response.body.errors[0].extensions.code).toBe('UNAUTHENTICATED');
});
});
Production Patterns
Schema Stitching
import { stitchSchemas } from '@graphql-tools/stitch';
const userSchema = makeExecutableSchema({ typeDefs: userTypeDefs, resolvers: userResolvers });
const postSchema = makeExecutableSchema({ typeDefs: postTypeDefs, resolvers: postResolvers });
const schema = stitchSchemas({
subschemas: [
{ schema: userSchema },
{ schema: postSchema },
],
});
Apollo Federation
import { buildSubgraphSchema } from '@apollo/subgraph';
const typeDefs = gql`
type User @key(fields: "id") {
id: ID!
name: String!
email: String!
}
`;
const resolvers = {
User: {
__resolveReference(user, { db }) {
return db.users.findById(user.id);
},
},
};
const schema = buildSubgraphSchema({ typeDefs, resolvers });
const typeDefs = gql`
extend type User @key(fields: "id") {
id: ID! @external
posts: [Post!]!
Post
ID
String
ID
`;
{ } ;
gateway = ({
: (, ),
});
server = ({ gateway });
Rate Limiting
import { GraphQLRateLimitDirective } from 'graphql-rate-limit-directive';
const typeDefs = gql`
directive @rateLimit(
limit: Int = 10
duration: Int = 60
) on FIELD_DEFINITION
type Query {
users: [User!]! @rateLimit(limit: 100, duration: 60)
search(query: String!): [Result!]! @rateLimit(limit: 10, duration: 60)
}
`;
let schema = ({ typeDefs, resolvers });
schema = ()(schema);
Monitoring (Apollo Studio)
import { ApolloServerPluginUsageReporting } from '@apollo/server/plugin/usageReporting';
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [
ApolloServerPluginUsageReporting({
sendVariableValues: { all: true },
sendHeaders: { all: true },
}),
],
});
Framework Integration
Next.js App Router
import { ApolloServer } from '@apollo/server';
import { startServerAndCreateNextHandler } from '@as-integrations/next';
const server = new ApolloServer({
typeDefs,
resolvers,
});
const handler = startServerAndCreateNextHandler(server);
export { handler as GET, handler as POST };
Next.js with Apollo Client (SSR)
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client';
import { registerApolloClient } from '@apollo/experimental-nextjs-app-support/rsc';
export const { getClient } = registerApolloClient(() => {
return new ApolloClient({
cache: new InMemoryCache(),
link: new HttpLink({
uri: 'http://localhost:4000/graphql',
}),
});
});
import { getClient } from '@/lib/apollo-client';
import { gql } from '@apollo/client';
export default async function UsersPage() {
const { data } = await getClient().query({
query: gql`
query GetUsers {
users {
id
name
}
}
`,
});
return (
);
}
FastAPI + Strawberry
from fastapi import FastAPI
from strawberry.fastapi import GraphQLRouter
app = FastAPI()
graphql_app = GraphQLRouter(schema)
app.include_router(graphql_app, prefix="/graphql")
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
Comparison with REST and tRPC
GraphQL vs REST
| Feature | GraphQL | REST |
|---|
| Endpoints | Single endpoint | Multiple endpoints |
| Data Fetching | Client specifies fields | Server determines response |
| Over-fetching | No | Yes (extra fields) |
| Under-fetching | No | Yes (multiple requests) |
| Versioning | Not needed | Required (v1, v2) |
| Caching | Complex | Simple (HTTP caching) |
| Type System | Built-in | External (OpenAPI) |
| Real-time | Subscriptions | SSE/WebSocket |
GraphQL vs tRPC
| Feature | GraphQL | tRPC |
|---|
| Type Safety | Codegen required | Native TypeScript |
| Language Support | Any | TypeScript only |
| Client-Server Coupling | Loose | Tight |
| Schema | SDL required | Inferred from code |
| Learning Curve | Steep | Gentle |
| Tooling | Extensive | Growing |
| Use Case | Public APIs, Mobile | Full-stack TypeScript |
Migration Strategies
REST to GraphQL (Gradual)
const resolvers = {
Query: {
user: async (_, { id }) => {
const response = await fetch(`https://api.example.com/users/${id}`);
return response.json();
},
},
};
app.use('/api/rest', restRouter);
app.use('/graphql', graphqlMiddleware);
Adding GraphQL to Existing App
import express from 'express';
import { expressMiddleware } from '@apollo/server/express4';
const app = express();
app.use('/api', existingApiRouter);
app.use('/graphql', express.json(), expressMiddleware(server));
Best Practices
Schema Design
- Use semantic field names (
createdAt, not created_at)
- Prefer specific types over generic JSON
- Use enums for fixed value sets
- Design for client use cases, not database structure
- Use input types for complex mutations
- Implement pagination for lists
- Follow Relay specification for connections
Resolver Patterns
- Keep resolvers thin, delegate to service layer
- Use DataLoader for all database fetches
- Validate inputs in resolvers, not database layer
- Return errors in payload, not just exceptions
- Use context for shared dependencies (db, auth, loaders)
Error Handling
- Use custom error types with error codes
- Return field-level errors for mutations
- Log errors server-side, sanitize for clients
- Use nullable fields to allow partial results
- Don't expose internal implementation details
Performance
- Always use DataLoader to prevent N+1
- Implement query complexity limits
- Cache frequently accessed data
- Use persisted queries in production
- Monitor slow queries and optimize
- Batch mutations when possible
Security
- Implement authentication and authorization
- Validate all inputs
- Use query depth limiting
- Implement rate limiting per user
- Disable introspection in production (optional)
- Sanitize error messages
Testing
- Test resolvers in isolation
- Mock external dependencies
- Test error conditions
- Integration test critical flows
- E2E test with real client
Documentation
- Write clear field descriptions
- Document deprecations with
@deprecated
- Provide usage examples in schema comments
- Keep schema documentation up-to-date
Summary
GraphQL provides a powerful, flexible API layer with strong typing, efficient data fetching, and excellent developer experience. Key advantages include:
- Client Control: Fetch exactly what you need
- Type Safety: Schema-first design with introspection
- Single Endpoint: Simplified API surface
- Real-time: Built-in subscription support
- Tooling: Excellent ecosystem (Apollo, Relay, codegen)
Trade-offs to Consider:
- More complex than REST for simple CRUD
- Caching requires more thought than HTTP caching
- Learning curve for teams new to GraphQL
- Query complexity can impact performance
Best For:
- Mobile apps needing bandwidth efficiency
- Complex frontends with varied data needs
- Microservices aggregation
- Real-time applications
- Multi-platform clients (web, mobile, IoT)
Start Simple:
- Define schema for core entities
- Write resolvers with DataLoader
- Add authentication/authorization
- Implement error handling
- Optimize with caching
- Add subscriptions if needed
- Monitor and iterate
GraphQL shines when API flexibility and developer experience are priorities. Combined with TypeScript code generation, it provides end-to-end type safety from database to UI.