apollo-server-patterns

Apollo Server Patterns

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "apollo-server-patterns" with this command: npx skills add thebushidocollective/han/thebushidocollective-han-apollo-server-patterns

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! } `;

  1. 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);
}

} };

  1. 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(); } }

  1. 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'); } }

  1. 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;
}

} };

  1. 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'); });

  1. 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);

  1. 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);
}

} };

  1. 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 }; } } };

  1. 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

Resources

  • Apollo Server Documentation

  • GraphQL Specification

  • Apollo Federation

  • DataLoader GitHub

  • GraphQL Best Practices

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

android-jetpack-compose

No summary provided by upstream source.

Repository SourceNeeds Review
General

fastapi-async-patterns

No summary provided by upstream source.

Repository SourceNeeds Review
General

storybook-story-writing

No summary provided by upstream source.

Repository SourceNeeds Review