GraphQL: From Basics to Production
Introduction
GraphQL is a query language for APIs that gives clients the power to ask for exactly what they need. This guide covers schema design, resolvers, mutations, subscriptions, and production best practices with Apollo Server.
1. GraphQL Basics
Setup Apollo Server
# Install dependencies
npm install @apollo/server graphql
# TypeScript support
npm install --save-dev @types/node typescript
# Create server
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
const typeDefs = `#graphql
type Query {
hello: String
}
`;
const resolvers = {
Query: {
hello: () => 'Hello world!',
},
};
const server = new ApolloServer({
typeDefs,
resolvers,
});
const { url } = await startStandaloneServer(server, {
listen: { port: 4000 },
});
console.log(`🚀 Server ready at: ${url}`);
2. Schema Definition
Types and Fields
type User {
id: ID!
email: String!
name: String!
age: Int
posts: [Post!]!
createdAt: String!
}
type Post {
id: ID!
title: String!
content: String!
published: Boolean!
author: User!
comments: [Comment!]!
}
type Comment {
id: ID!
text: String!
author: User!
post: Post!
}
# Query root type
type Query {
users: [User!]!
user(id: ID!): User
posts(published: Boolean): [Post!]!
post(id: ID!): Post
}
# Mutation root type
type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User!
deleteUser(id: ID!): Boolean!
createPost(input: CreatePostInput!): Post!
}
# Input types
input CreateUserInput {
email: String!
name: String!
password: String!
}
input UpdateUserInput {
email: String
name: String
}
input CreatePostInput {
title: String!
content: String!
authorId: ID!
}
3. Resolvers
Query Resolvers
import User from './models/User';
import Post from './models/Post';
const resolvers = {
Query: {
users: async () => {
return await User.find();
},
user: async (parent, { id }) => {
return await User.findById(id);
},
posts: async (parent, { published }) => {
const filter = published !== undefined ? { published } : {};
return await Post.find(filter);
},
post: async (parent, { id }) => {
return await Post.findById(id);
},
},
// Field resolvers
User: {
posts: async (parent) => {
return await Post.find({ authorId: parent.id });
},
},
Post: {
author: async (parent) => {
return await User.findById(parent.authorId);
},
comments: async (parent) => {
return await Comment.find({ postId: parent.id });
},
},
};
Mutation Resolvers
const resolvers = {
Mutation: {
createUser: async (parent, { input }, context) => {
// Validate input
if (!input.email || !input.password) {
throw new GraphQLError('Email and password are required', {
extensions: { code: 'BAD_USER_INPUT' }
});
}
// Check if user exists
const existingUser = await User.findOne({ email: input.email });
if (existingUser) {
throw new GraphQLError('User already exists', {
extensions: { code: 'USER_EXISTS' }
});
}
// Hash password
const hashedPassword = await bcrypt.hash(input.password, 10);
// Create user
const user = await User.create({
...input,
password: hashedPassword,
});
return user;
},
updateUser: async (parent, { id, input }, context) => {
// Check authentication
if (!context.user) {
throw new GraphQLError('Not authenticated', {
extensions: { code: 'UNAUTHENTICATED' }
});
}
// Check authorization
if (context.user.id !== id) {
throw new GraphQLError('Not authorized', {
extensions: { code: 'FORBIDDEN' }
});
}
const user = await User.findByIdAndUpdate(
id,
{ $set: input },
{ new: true }
);
return user;
},
deleteUser: async (parent, { id }, context) => {
await User.findByIdAndDelete(id);
return true;
},
},
};
4. Context & Authentication
import jwt from 'jsonwebtoken';
const server = new ApolloServer({
typeDefs,
resolvers,
});
const { url } = await startStandaloneServer(server, {
context: async ({ req }) => {
// Get token from header
const token = req.headers.authorization?.replace('Bearer ', '');
if (token) {
try {
// Verify token
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const user = await User.findById(decoded.userId);
return { user };
} catch (error) {
console.log('Invalid token');
}
}
return { user: null };
},
listen: { port: 4000 },
});
5. DataLoader (N+1 Problem)
import DataLoader from 'dataloader';
// Create loader
const userLoader = new DataLoader(async (userIds) => {
const users = await User.find({ _id: { $in: userIds } });
// Return users in same order as requested IDs
const userMap = {};
users.forEach(user => {
userMap[user.id] = user;
});
return userIds.map(id => userMap[id]);
});
// Use in context
const { url } = await startStandaloneServer(server, {
context: async ({ req }) => {
return {
loaders: {
user: userLoader,
},
};
},
});
// Use in resolver
const resolvers = {
Post: {
author: async (parent, args, context) => {
return await context.loaders.user.load(parent.authorId);
},
},
};
6. Subscriptions
import { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import { PubSub } from 'graphql-subscriptions';
const pubsub = new PubSub();
// Add to schema
const typeDefs = `#graphql
type Subscription {
postCreated: Post!
commentAdded(postId: ID!): Comment!
}
`;
// Add resolvers
const resolvers = {
Subscription: {
postCreated: {
subscribe: () => pubsub.asyncIterator(['POST_CREATED']),
},
commentAdded: {
subscribe: (parent, { postId }) => {
return pubsub.asyncIterator([`COMMENT_ADDED_${postId}`]);
},
},
},
Mutation: {
createPost: async (parent, { input }) => {
const post = await Post.create(input);
// Publish event
pubsub.publish('POST_CREATED', { postCreated: post });
return post;
},
},
};
// Setup WebSocket server
const wsServer = new WebSocketServer({
server: httpServer,
path: '/graphql',
});
useServer({ schema }, wsServer);
7. Error Handling
import { GraphQLError } from 'graphql';
// Custom error codes
export enum ErrorCode {
UNAUTHENTICATED = 'UNAUTHENTICATED',
FORBIDDEN = 'FORBIDDEN',
BAD_USER_INPUT = 'BAD_USER_INPUT',
NOT_FOUND = 'NOT_FOUND',
}
// Helper function
export const throwGraphQLError = (message: string, code: ErrorCode) => {
throw new GraphQLError(message, {
extensions: { code }
});
};
// Usage in resolver
const resolvers = {
Query: {
user: async (parent, { id }) => {
const user = await User.findById(id);
if (!user) {
throwGraphQLError('User not found', ErrorCode.NOT_FOUND);
}
return user;
},
},
};
// Format errors
const server = new ApolloServer({
typeDefs,
resolvers,
formatError: (formattedError, error) => {
// Don't expose internal errors
if (formattedError.extensions?.code === 'INTERNAL_SERVER_ERROR') {
return {
message: 'Internal server error',
extensions: {
code: 'INTERNAL_SERVER_ERROR',
},
};
}
return formattedError;
},
});
8. Pagination
// Relay-style cursor pagination
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
type PostEdge {
cursor: String!
node: Post!
}
type PostConnection {
edges: [PostEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type Query {
posts(first: Int, after: String, last: Int, before: String): PostConnection!
}
// Resolver
const resolvers = {
Query: {
posts: async (parent, { first = 10, after }) => {
const query = after ? { _id: { $gt: after } } : {};
const posts = await Post.find(query)
.limit(first + 1)
.sort({ _id: 1 });
const hasNextPage = posts.length > first;
const edges = posts.slice(0, first).map(post => ({
cursor: post.id,
node: post,
}));
return {
edges,
pageInfo: {
hasNextPage,
hasPreviousPage: !!after,
startCursor: edges[0]?.cursor,
endCursor: edges[edges.length - 1]?.cursor,
},
totalCount: await Post.countDocuments(),
};
},
},
};
9. Performance Optimization
// Query complexity
import { createComplexityLimitRule } from 'graphql-validation-complexity';
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
createComplexityLimitRule(1000, {
onCost: (cost) => console.log('Query cost:', cost),
}),
],
});
// Query depth limiting
import depthLimit from 'graphql-depth-limit';
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [depthLimit(5)],
});
// Caching with Redis
import { BaseRedisCache } from 'apollo-server-cache-redis';
import Redis from 'ioredis';
const server = new ApolloServer({
typeDefs,
resolvers,
cache: new BaseRedisCache({
client: new Redis({
host: 'localhost',
}),
}),
plugins: [
{
async requestDidStart() {
return {
async willSendResponse({ response, context }) {
response.http.headers.set(
'Cache-Control',
'max-age=60, public'
);
},
};
},
},
],
});
10. Client Usage
// Apollo Client setup
import { ApolloClient, InMemoryCache, gql } from '@apollo/client';
const client = new ApolloClient({
uri: 'http://localhost:4000/graphql',
cache: new InMemoryCache(),
headers: {
authorization: `Bearer ${token}`,
},
});
// Query
const GET_USERS = gql`
query GetUsers {
users {
id
name
email
posts {
id
title
}
}
}
`;
const { data } = await client.query({ query: GET_USERS });
// Mutation
const CREATE_USER = gql`
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
name
email
}
}
`;
const { data } = await client.mutate({
mutation: CREATE_USER,
variables: {
input: {
email: 'user@example.com',
name: 'John Doe',
password: 'password123',
},
},
});
Best Practices
✓ GraphQL Best Practices:
- Use DataLoader to solve N+1 queries
- Implement proper error handling with codes
- Add authentication at context level
- Use input types for mutations
- Implement pagination for lists
- Set query complexity limits
- Limit query depth (5-10 levels max)
- Use subscriptions for real-time updates
- Cache responses when appropriate
- Document schema with descriptions
- Version schema carefully (deprecate fields)
- Monitor query performance
- Use persisted queries in production
- Implement rate limiting
- Test resolvers thoroughly
Conclusion
GraphQL provides powerful query capabilities and type safety, making it ideal for modern applications. By implementing DataLoader, proper error handling, authentication, and performance optimizations, you can build scalable GraphQL APIs that provide excellent developer experience.
💡 Pro Tip: Always use DataLoader for any field that fetches related data. Monitor your GraphQL queries in production with Apollo Studio to identify slow resolvers and optimize performance bottlenecks.