Awesome-omni-skill apollo-server-patterns
Use when building GraphQL APIs with Apollo Server requiring resolvers, data sources, schema design, and federation.
install
source · Clone the upstream repo
git clone https://github.com/diegosouzapw/awesome-omni-skill
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/diegosouzapw/awesome-omni-skill "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/development/apollo-server-patterns" ~/.claude/skills/diegosouzapw-awesome-omni-skill-apollo-server-patterns && rm -rf "$T"
manifest:
skills/development/apollo-server-patterns/SKILL.mdsource content
Apollo Server Patterns
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