Apollo Server
Build GraphQL APIs with Apollo Server - resolvers, context, and production patterns.
7 min read
Setup#
bash
npm install @apollo/server graphql
Basic Server#
javascript
// src/index.js
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
// Schema
const typeDefs = `#graphql
type User {
id: ID!
email: String!
name: String!
}
type Query {
users: [User!]!
user(id: ID!): User
}
type Mutation {
createUser(email: String!, name: String!): User!
}
`;
// Resolvers
const resolvers = {
Query: {
users: () => User.find(),
user: (_, { id }) => User.findById(id),
},
Mutation: {
createUser: (_, { email, name }) => User.create({ email, name }),
},
};
// Server
const server = new ApolloServer({ typeDefs, resolvers });
const { url } = await startStandaloneServer(server, {
listen: { port: 4000 },
});
console.log(`GraphQL server at ${url}`);
With Express#
javascript
import express from 'express';
import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@apollo/server/express4';
import cors from 'cors';
const app = express();
const server = new ApolloServer({ typeDefs, resolvers });
await server.start();
app.use(cors());
app.use(express.json());
app.use('/graphql', expressMiddleware(server, {
context: async ({ req }) => ({
user: await getUserFromToken(req.headers.authorization),
}),
}));
app.listen(4000);
Complete Example#
Schema#
javascript
// src/schema/typeDefs.js
export const typeDefs = `#graphql
type User {
id: ID!
email: String!
name: String!
avatar: String
posts: [Post!]!
createdAt: String!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
comments: [Comment!]!
createdAt: String!
}
type Comment {
id: ID!
text: String!
author: User!
post: Post!
createdAt: String!
}
type AuthPayload {
token: String!
user: User!
}
type Query {
# Users
me: User
user(id: ID!): User
users(page: Int, limit: Int): UsersConnection!
# Posts
post(id: ID!): Post
posts(authorId: ID, page: Int, limit: Int): PostsConnection!
}
type Mutation {
# Auth
signup(input: SignupInput!): AuthPayload!
login(email: String!, password: String!): AuthPayload!
# Users
updateProfile(input: UpdateProfileInput!): User!
# Posts
createPost(input: CreatePostInput!): Post!
updatePost(id: ID!, input: UpdatePostInput!): Post!
deletePost(id: ID!): Boolean!
# Comments
addComment(postId: ID!, text: String!): Comment!
}
type Subscription {
commentAdded(postId: ID!): Comment!
}
# Inputs
input SignupInput {
email: String!
password: String!
name: String!
}
input UpdateProfileInput {
name: String
avatar: String
}
input CreatePostInput {
title: String!
content: String!
}
input UpdatePostInput {
title: String
content: String
}
# Pagination
type UsersConnection {
nodes: [User!]!
pageInfo: PageInfo!
}
type PostsConnection {
nodes: [Post!]!
pageInfo: PageInfo!
}
type PageInfo {
page: Int!
limit: Int!
total: Int!
hasNextPage: Boolean!
}
`;
Resolvers#
javascript
// src/schema/resolvers.js
export const resolvers = {
Query: {
me: (_, __, { user }) => {
if (!user) return null;
return User.findById(user.id);
},
user: (_, { id }) => User.findById(id),
users: async (_, { page = 1, limit = 20 }) => {
const skip = (page - 1) * limit;
const [nodes, total] = await Promise.all([
User.find().skip(skip).limit(limit),
User.countDocuments(),
]);
return {
nodes,
pageInfo: {
page,
limit,
total,
hasNextPage: skip + nodes.length < total,
},
};
},
post: (_, { id }) => Post.findById(id),
posts: async (_, { authorId, page = 1, limit = 20 }) => {
const filter = authorId ? { author: authorId } : {};
const skip = (page - 1) * limit;
const [nodes, total] = await Promise.all([
Post.find(filter).skip(skip).limit(limit).sort({ createdAt: -1 }),
Post.countDocuments(filter),
]);
return {
nodes,
pageInfo: { page, limit, total, hasNextPage: skip + nodes.length < total },
};
},
},
Mutation: {
signup: async (_, { input }) => {
const user = await UserService.create(input);
const token = generateToken(user);
return { token, user };
},
login: async (_, { email, password }) => {
const user = await UserService.authenticate(email, password);
if (!user) {
throw new GraphQLError('Invalid credentials', {
extensions: { code: 'UNAUTHENTICATED' },
});
}
const token = generateToken(user);
return { token, user };
},
createPost: async (_, { input }, { user }) => {
if (!user) {
throw new GraphQLError('Must be logged in', {
extensions: { code: 'UNAUTHENTICATED' },
});
}
return Post.create({ ...input, author: user.id });
},
deletePost: async (_, { id }, { user }) => {
const post = await Post.findById(id);
if (!post) return false;
if (post.author.toString() !== user.id) {
throw new GraphQLError('Not authorized', {
extensions: { code: 'FORBIDDEN' },
});
}
await post.deleteOne();
return true;
},
},
// Field resolvers
User: {
posts: (user) => Post.find({ author: user.id }),
},
Post: {
author: (post) => User.findById(post.author),
comments: (post) => Comment.find({ post: post.id }),
},
Comment: {
author: (comment) => User.findById(comment.author),
post: (comment) => Post.findById(comment.post),
},
};
Context & Authentication#
javascript
// src/context.js
import { GraphQLError } from 'graphql';
import jwt from 'jsonwebtoken';
export async function createContext({ req }) {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return { user: null };
}
try {
const payload = jwt.verify(token, process.env.JWT_SECRET);
const user = await User.findById(payload.userId);
return { user };
} catch {
return { user: null };
}
}
// Helper for resolvers
export function requireAuth(user) {
if (!user) {
throw new GraphQLError('Authentication required', {
extensions: { code: 'UNAUTHENTICATED' },
});
}
return user;
}
DataLoader (N+1 Prevention)#
bash
npm install dataloader
javascript
// src/loaders.js
import DataLoader from 'dataloader';
export function createLoaders() {
return {
userLoader: new DataLoader(async (ids) => {
const users = await User.find({ _id: { $in: ids } });
const userMap = new Map(users.map(u => [u.id.toString(), u]));
return ids.map(id => userMap.get(id.toString()));
}),
postsByAuthorLoader: new DataLoader(async (authorIds) => {
const posts = await Post.find({ author: { $in: authorIds } });
const postMap = new Map();
for (const post of posts) {
const authorId = post.author.toString();
if (!postMap.has(authorId)) postMap.set(authorId, []);
postMap.get(authorId).push(post);
}
return authorIds.map(id => postMap.get(id.toString()) || []);
}),
};
}
// Add to context
export async function createContext({ req }) {
return {
user: await getUserFromToken(req),
loaders: createLoaders(),
};
}
// Use in resolvers
const resolvers = {
Post: {
author: (post, _, { loaders }) => loaders.userLoader.load(post.author.toString()),
},
User: {
posts: (user, _, { loaders }) => loaders.postsByAuthorLoader.load(user.id.toString()),
},
};
Error Handling#
javascript
import { GraphQLError } from 'graphql';
// Custom errors
export class NotFoundError extends GraphQLError {
constructor(resource) {
super(`${resource} not found`, {
extensions: { code: 'NOT_FOUND' },
});
}
}
export class ValidationError extends GraphQLError {
constructor(message, fields) {
super(message, {
extensions: {
code: 'VALIDATION_ERROR',
fields,
},
});
}
}
// Format errors
const server = new ApolloServer({
typeDefs,
resolvers,
formatError: (error) => {
// Log server errors
if (error.extensions?.code === 'INTERNAL_SERVER_ERROR') {
console.error(error);
return { message: 'Internal server error' };
}
return error;
},
});
Subscriptions#
bash
npm install graphql-ws ws
javascript
import { createServer } from 'http';
import { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import { makeExecutableSchema } from '@graphql-tools/schema';
import { PubSub } from 'graphql-subscriptions';
const pubsub = new PubSub();
const COMMENT_ADDED = 'COMMENT_ADDED';
const resolvers = {
Mutation: {
addComment: async (_, { postId, text }, { user }) => {
const comment = await Comment.create({
post: postId,
author: user.id,
text,
});
// Publish event
pubsub.publish(COMMENT_ADDED, {
commentAdded: comment,
postId,
});
return comment;
},
},
Subscription: {
commentAdded: {
subscribe: (_, { postId }) => ({
[Symbol.asyncIterator]: () => pubsub.asyncIterator(COMMENT_ADDED),
}),
resolve: (payload, { postId }) => {
if (payload.postId === postId) {
return payload.commentAdded;
}
},
},
},
};
// Setup
const schema = makeExecutableSchema({ typeDefs, resolvers });
const httpServer = createServer(app);
const wsServer = new WebSocketServer({ server: httpServer, path: '/graphql' });
useServer({ schema }, wsServer);
Key Takeaways#
- Schema-first - Define types before resolvers
- Use DataLoader - Prevent N+1 queries
- Context for auth - Pass user to all resolvers
- Custom errors - Use GraphQLError with codes
- Subscriptions for real-time - WebSocket-based
Continue Learning
Ready to level up your skills?
Explore more guides and tutorials to deepen your understanding and become a better developer.