ð apollo-server-patterns
Use when building GraphQL APIs with Apollo Server requiring resolvers, data sources, schema design, and federation.
Overview
Master Apollo Server for building production-ready GraphQL APIs with proper schema design, efficient resolvers, and scalable architecture.
Overview
Apollo Server is a spec-compliant GraphQL server that works with any GraphQL schema. It provides features like schema stitching, federation, data sources, and built-in monitoring for production GraphQL APIs.
Installation and Setup
Installing Apollo Server
# For Express
npm install @apollo/server graphql express cors body-parser
# For standalone server
npm install @apollo/server graphql
# Additional utilities
npm install graphql-tag dataloader
Basic Server Setup
// server.js
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { typeDefs } from './schema.js';
import { resolvers } from './resolvers.js';
const server = new ApolloServer({
typeDefs,
resolvers,
formatError: (formattedError, error) => {
// Custom error formatting
if (formattedError.extensions?.code === 'INTERNAL_SERVER_ERROR') {
return {
...formattedError,
message: 'An internal error occurred'
};
}
return formattedError;
},
plugins: [
{
async requestDidStart() {
return {
async willSendResponse({ response }) {
console.log('Response sent');
}
};
}
}
]
});
const { url } = await startStandaloneServer(server, {
listen: { port: 4000 },
context: async ({ req }) => {
const token = req.headers.authorization || '';
const user = await getUserFromToken(token);
return { user };
}
});
console.log(`Server ready at ${url}`);
Core Patterns
1. Schema Definition
// schema.js
import { gql } from 'graphql-tag';
export const typeDefs = gql`
type User {
id: ID!
email: String!
name: String!
posts: [Post!]!
createdAt: String!
}
type Post {
id: ID!
title: String!
body: String!
author: User!
comments: [Comment!]!
published: Boolean!
createdAt: String!
updatedAt: String!
}
type Comment {
id: ID!
body: String!
author: User!
post: Post!
createdAt: String!
}
input CreatePostInput {
title: String!
body: String!
}
input UpdatePostInput {
title: String
body: String
published: Boolean
}
type Query {
me: User
user(id: ID!): User
users(limit: Int, offset: Int): [User!]!
post(id: ID!): Post
posts(published: Boolean, authorId: ID): [Post!]!
}
type Mutation {
signup(email: String!, password: String!, name: String!): AuthPayload!
login(email: String!, password: String!): AuthPayload!
createPost(input: CreatePostInput!): Post!
updatePost(id: ID!, input: UpdatePostInput!): Post!
deletePost(id: ID!): Boolean!
createComment(postId: ID!, body: String!): Comment!
}
type Subscription {
postCreated: Post!
commentAdded(postId: ID!): Comment!
}
type AuthPayload {
token: String!
user: User!
}
`;
2. Resolvers
// resolvers.js
export const resolvers = {
Query: {
me: (parent, args, context) => {
if (!context.user) {
throw new Error('Not authenticated');
}
return context.user;
},
user: async (parent, { id }, { dataSources }) => {
return dataSources.usersAPI.getUserById(id);
},
users: async (parent, { limit = 10, offset = 0 }, { dataSources }) => {
return dataSources.usersAPI.getUsers({ limit, offset });
},
post: async (parent, { id }, { dataSources }) => {
return dataSources.postsAPI.getPostById(id);
},
posts: async (parent, { published, authorId }, { dataSources }) => {
return dataSources.postsAPI.getPosts({ published, authorId });
}
},
Mutation: {
signup: async (parent, { email, password, name }, { dataSources }) => {
const user = await dataSources.usersAPI.createUser({
email,
password,
name
});
const token = generateToken(user);
return { token, user };
},
login: async (parent, { email, password }, { dataSources }) => {
const user = await dataSources.usersAPI.authenticate(email, password);
if (!user) {
throw new Error('Invalid credentials');
}
const token = generateToken(user);
return { token, user };
},
createPost: async (parent, { input }, { user, dataSources }) => {
if (!user) {
throw new Error('Not authenticated');
}
return dataSources.postsAPI.createPost({
...input,
authorId: user.id
});
},
updatePost: async (parent, { id, input }, { user, dataSources }) => {
const post = await dataSources.postsAPI.getPostById(id);
if (post.authorId !== user.id) {
throw new Error('Not authorized');
}
return dataSources.postsAPI.updatePost(id, input);
},
deletePost: async (parent, { id }, { user, dataSources }) => {
const post = await dataSources.postsAPI.getPostById(id);
if (post.authorId !== user.id) {
throw new Error('Not authorized');
}
await dataSources.postsAPI.deletePost(id);
return true;
}
},
// Field resolvers
User: {
posts: async (parent, args, { dataSources }) => {
return dataSources.postsAPI.getPostsByAuthorId(parent.id);
}
},
Post: {
author: async (parent, args, { dataSources }) => {
return dataSources.usersAPI.getUserById(parent.authorId);
},
comments: async (parent, args, { dataSources }) => {
return dataSources.commentsAPI.getCommentsByPostId(parent.id);
}
},
Comment: {
author: async (parent, args, { dataSources }) => {
return dataSources.usersAPI.getUserById(parent.authorId);
},
post: async (parent, args, { dataSources }) => {
return dataSources.postsAPI.getPostById(parent.postId);
}
}
};
3. Data Sources
// dataSources/UsersAPI.js
import { RESTDataSource } from '@apollo/datasource-rest';
export class UsersAPI extends RESTDataSource {
constructor() {
super();
this.baseURL = 'https://api.example.com/';
}
async getUserById(id) {
return this.get(`users/${id}`);
}
async getUsers({ limit, offset }) {
return this.get('users', {
params: { limit, offset }
});
}
async createUser({ email, password, name }) {
return this.post('users', {
body: { email, password, name }
});
}
async authenticate(email, password) {
try {
const response = await this.post('auth/login', {
body: { email, password }
});
return response.user;
} catch (error) {
return null;
}
}
}
// dataSources/PostsDB.js
import DataLoader from 'dataloader';
export class PostsDB {
constructor(db) {
this.db = db;
this.loader = new DataLoader(this.batchGetPosts.bind(this));
}
async batchGetPosts(ids) {
const posts = await this.db
.select('*')
.from('posts')
.whereIn('id', ids);
// Return posts in same order as ids
return ids.map(id => posts.find(post => post.id === id));
}
async getPostById(id) {
return this.loader.load(id);
}
async getPosts({ published, authorId }) {
let query = this.db.select('*').from('posts');
if (published !== undefined) {
query = query.where('published', published);
}
if (authorId) {
query = query.where('author_id', authorId);
}
return query;
}
async getPostsByAuthorId(authorId) {
return this.db
.select('*')
.from('posts')
.where('author_id', authorId);
}
async createPost({ title, body, authorId }) {
const [post] = await this.db('posts')
.insert({
title,
body,
author_id: authorId,
published: false,
created_at: new Date(),
updated_at: new Date()
})
.returning('*');
return post;
}
async updatePost(id, updates) {
const [post] = await this.db('posts')
.where('id', id)
.update({
...updates,
updated_at: new Date()
})
.returning('*');
return post;
}
async deletePost(id) {
await this.db('posts').where('id', id).delete();
}
}
4. Context and Authentication
// context.js
import jwt from 'jsonwebtoken';
import { UsersAPI } from './dataSources/UsersAPI.js';
import { PostsDB } from './dataSources/PostsDB.js';
import { CommentsDB } from './dataSources/CommentsDB.js';
export async function createContext({ req }) {
// Extract token from header
const token = req.headers.authorization?.replace('Bearer ', '') || '';
// Verify and decode token
let user = null;
if (token) {
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
user = await getUserById(decoded.userId);
} catch (error) {
console.error('Invalid token:', error);
}
}
// Create data sources
const dataSources = {
usersAPI: new UsersAPI(),
postsDB: new PostsDB(db),
commentsDB: new CommentsDB(db)
};
return {
user,
dataSources,
db
};
}
// Authorization helpers
export function requireAuth(user) {
if (!user) {
throw new Error('Not authenticated');
}
}
export function requireRole(user, role) {
requireAuth(user);
if (user.role !== role) {
throw new Error('Not authorized');
}
}
5. Error Handling
// errors.js
import { GraphQLError } from 'graphql';
export class AuthenticationError extends GraphQLError {
constructor(message) {
super(message, {
extensions: {
code: 'UNAUTHENTICATED',
http: { status: 401 }
}
});
}
}
export class ForbiddenError extends GraphQLError {
constructor(message) {
super(message, {
extensions: {
code: 'FORBIDDEN',
http: { status: 403 }
}
});
}
}
export class ValidationError extends GraphQLError {
constructor(message, fields) {
super(message, {
extensions: {
code: 'BAD_USER_INPUT',
validationErrors: fields,
http: { status: 400 }
}
});
}
}
// Usage in resolvers
import { AuthenticationError, ForbiddenError } from './errors.js';
const resolvers = {
Mutation: {
deletePost: async (parent, { id }, { user, dataSources }) => {
if (!user) {
throw new AuthenticationError('You must be logged in');
}
const post = await dataSources.postsDB.getPostById(id);
if (post.authorId !== user.id) {
throw new ForbiddenError('You can only delete your own posts');
}
await dataSources.postsDB.deletePost(id);
return true;
}
}
};
6. Subscriptions
// server-with-subscriptions.js
import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@apollo/server/express4';
import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer';
import { createServer } from 'http';
import express from 'express';
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 typeDefs = gql`
type Subscription {
postCreated: Post!
commentAdded(postId: ID!): Comment!
}
`;
const resolvers = {
Mutation: {
createPost: async (parent, { input }, { user, dataSources }) => {
const post = await dataSources.postsDB.createPost({
...input,
authorId: user.id
});
// Publish subscription event
pubsub.publish('POST_CREATED', { postCreated: post });
return post;
},
createComment: async (parent, { postId, body }, { user, dataSources }) => {
const comment = await dataSources.commentsDB.createComment({
postId,
body,
authorId: user.id
});
pubsub.publish(`COMMENT_ADDED_${postId}`, { commentAdded: comment });
return comment;
}
},
Subscription: {
postCreated: {
subscribe: () => pubsub.asyncIterator(['POST_CREATED'])
},
commentAdded: {
subscribe: (parent, { postId }) =>
pubsub.asyncIterator([`COMMENT_ADDED_${postId}`])
}
}
};
// Create schema
const schema = makeExecutableSchema({ typeDefs, resolvers });
// Create HTTP server
const app = express();
const httpServer = createServer(app);
// Create WebSocket server
const wsServer = new WebSocketServer({
server: httpServer,
path: '/graphql'
});
const serverCleanup = useServer({ schema }, wsServer);
// Create Apollo Server
const server = new ApolloServer({
schema,
plugins: [
ApolloServerPluginDrainHttpServer({ httpServer }),
{
async serverWillStart() {
return {
async drainServer() {
await serverCleanup.dispose();
}
};
}
}
]
});
await server.start();
app.use(
'/graphql',
cors(),
express.json(),
expressMiddleware(server, {
context: createContext
})
);
httpServer.listen(4000, () => {
console.log('Server running on http://localhost:4000/graphql');
});
7. Schema Directives
// directives.js
import { mapSchema, getDirective, MapperKind } from '@graphql-tools/utils';
import { defaultFieldResolver } from 'graphql';
// Define directive in schema
const typeDefs = gql`
directive @auth(requires: Role = USER) on FIELD_DEFINITION | OBJECT
enum Role {
ADMIN
USER
GUEST
}
type Query {
me: User @auth
users: [User!]! @auth(requires: ADMIN)
}
`;
// Implement directive
function authDirective(directiveName) {
return {
authDirectiveTypeDefs: `directive @${directiveName}(requires: Role = USER)
on FIELD_DEFINITION | OBJECT`,
authDirectiveTransformer: (schema) =>
mapSchema(schema, {
[MapperKind.OBJECT_FIELD]: (fieldConfig) => {
const authDirective = getDirective(
schema,
fieldConfig,
directiveName
)?.[0];
if (authDirective) {
const { requires } = authDirective;
const { resolve = defaultFieldResolver } = fieldConfig;
fieldConfig.resolve = async function (source, args, context, info) {
const { user } = context;
if (!user) {
throw new Error('Not authenticated');
}
if (requires && user.role !== requires) {
throw new Error(`Requires ${requires} role`);
}
return resolve(source, args, context, info);
};
}
return fieldConfig;
}
})
};
}
// Apply to schema
const { authDirectiveTypeDefs, authDirectiveTransformer } = authDirective('auth');
let schema = makeExecutableSchema({
typeDefs: [authDirectiveTypeDefs, typeDefs],
resolvers
});
schema = authDirectiveTransformer(schema);
8. Batching and Caching with DataLoader
// loaders.js
import DataLoader from 'dataloader';
export function createLoaders(db) {
// Batch load users
const userLoader = new DataLoader(async (userIds) => {
const users = await db
.select('*')
.from('users')
.whereIn('id', userIds);
return userIds.map(id => users.find(user => user.id === id));
});
// Batch load posts with caching
const postLoader = new DataLoader(
async (postIds) => {
const posts = await db
.select('*')
.from('posts')
.whereIn('id', postIds);
return postIds.map(id => posts.find(post => post.id === id));
},
{
// Cache for 5 minutes
cacheMap: new Map(),
cacheKeyFn: (key) => key,
batch: true,
maxBatchSize: 100
}
);
// Load comments by post ID (one-to-many)
const commentsByPostLoader = new DataLoader(async (postIds) => {
const comments = await db
.select('*')
.from('comments')
.whereIn('post_id', postIds);
return postIds.map(postId =>
comments.filter(comment => comment.post_id === postId)
);
});
return {
userLoader,
postLoader,
commentsByPostLoader
};
}
// Use in context
export async function createContext({ req }) {
const loaders = createLoaders(db);
return {
loaders,
// ... other context
};
}
// Use in resolvers
const resolvers = {
Post: {
author: (parent, args, { loaders }) => {
return loaders.userLoader.load(parent.authorId);
},
comments: (parent, args, { loaders }) => {
return loaders.commentsByPostLoader.load(parent.id);
}
}
};
9. Federation
// subgraph-users.js
import { ApolloServer } from '@apollo/server';
import { buildSubgraphSchema } from '@apollo/subgraph';
import gql from 'graphql-tag';
const typeDefs = gql`
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.0",
import: ["@key", "@shareable"])
type User @key(fields: "id") {
id: ID!
email: String!
name: String!
}
type Query {
user(id: ID!): User
users: [User!]!
}
`;
const resolvers = {
Query: {
user: (parent, { id }, { dataSources }) => {
return dataSources.usersDB.getUserById(id);
},
users: (parent, args, { dataSources }) => {
return dataSources.usersDB.getUsers();
}
},
User: {
__resolveReference: (user, { dataSources }) => {
return dataSources.usersDB.getUserById(user.id);
}
}
};
const server = new ApolloServer({
schema: buildSubgraphSchema({ typeDefs, resolvers })
});
// subgraph-posts.js
const typeDefs = gql`
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.0",
import: ["@key"])
type User @key(fields: "id") {
id: ID!
posts: [Post!]!
}
type Post @key(fields: "id") {
id: ID!
title: String!
body: String!
author: User!
}
type Query {
post(id: ID!): Post
posts: [Post!]!
}
`;
const resolvers = {
User: {
posts: (user, args, { dataSources }) => {
return dataSources.postsDB.getPostsByAuthorId(user.id);
}
},
Post: {
author: (post) => {
return { __typename: 'User', id: post.authorId };
}
}
};
10. Performance Monitoring
// plugins/monitoring.js
export const monitoringPlugin = {
async requestDidStart() {
const start = Date.now();
return {
async willSendResponse({ response, errors }) {
const duration = Date.now() - start;
console.log({
duration,
hasErrors: !!errors,
operationName: request.operationName
});
// Send to monitoring service
if (duration > 1000) {
await metrics.recordSlowQuery({
operation: request.operationName,
duration
});
}
},
async didEncounterErrors({ errors }) {
errors.forEach(error => {
console.error('GraphQL Error:', error);
// Send to error tracking service
errorTracker.captureException(error);
});
}
};
}
};
// Usage
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [monitoringPlugin]
});
Best Practices
- Use DataLoader - Batch and cache database queries
- Implement proper auth - Secure resolvers with authentication
- Design schema carefully - Think about client needs first
- Use input types - Validate mutation inputs properly
- Handle errors gracefully - Return meaningful error messages
- Implement monitoring - Track performance and errors
- Use data sources - Separate data fetching logic
- Leverage federation - Split large schemas into subgraphs
- Cache appropriately - Use Redis for shared cache
- Document schema - Add descriptions to types and fields
Common Pitfalls
- N+1 query problems - Not using DataLoader for batching
- Over-fetching in resolvers - Loading unnecessary data
- Missing error handling - Not catching and formatting errors
- Poor schema design - Not following GraphQL best practices
- No authentication - Exposing sensitive data without auth
- Blocking operations - Synchronous operations in resolvers
- Memory leaks - Not cleaning up subscriptions
- Missing validation - Not validating input data
- Exposing internals - Leaking database errors to clients
- No rate limiting - Allowing unlimited query complexity
When to Use
- Building GraphQL APIs
- Creating microservices with federation
- Developing real-time applications
- Building mobile backends
- Creating unified API gateways
- Developing admin dashboards
- Building e-commerce platforms
- Creating content management systems
- Developing social platforms
- Building analytics APIs