Apollo Server
Apollo Server is the most popular Node GraphQL server. Schema-first or code-first, batteries-included plugins (caching, tracing, federation), strong TypeScript integration.
Setup, context, datasources, subscriptions
EXAMPLE
// 1) Install + minimal server
// npm i @apollo/server graphql
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
const typeDefs = /* GraphQL */ `
type User {
id: ID!
name: String!
email: String!
}
type Query {
users: [User!]!
user(id: ID!): User
}
`;
const resolvers = {
Query: {
users: () => db.users.findAll(),
user: (_, { id }) => db.users.findById(id),
},
};
const server = new ApolloServer({ typeDefs, resolvers });
const { url } = await startStandaloneServer(server, { listen: { port: 4000 } });
console.log(`Ready at ${url}`);
// 2) With Express — useful when you need other routes
import express from 'express';
import http from 'node:http';
import { expressMiddleware } from '@apollo/server/express4';
import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer';
import cors from 'cors';
import { json } from 'body-parser';
const app = express();
const http_ = http.createServer(app);
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [ApolloServerPluginDrainHttpServer({ httpServer: http_ })],
});
await server.start();
app.use('/graphql',
cors(),
json(),
expressMiddleware(server, {
context: async ({ req }) => ({
user: await verifyToken(req.headers.authorization),
db,
loaders: createLoaders(db),
}),
}),
);
http_.listen(4000);
// 3) Context — per-request shared data
async function createContext({ req }) {
const token = req.headers.authorization?.replace('Bearer ', '');
const user = token ? await verifyToken(token) : null;
return {
user,
db,
loaders: createLoaders(db),
ip: req.ip,
reqId: req.headers['x-request-id'] ?? randomUUID(),
};
}
// 4) Schema directives (auth example)
const typeDefs = /* GraphQL */ `
directive @auth(requires: Role = USER) on FIELD_DEFINITION
enum Role { ADMIN USER GUEST }
type Query {
me: User! @auth
secrets: [String!]! @auth(requires: ADMIN)
public: String
}
`;
import { mapSchema, getDirective, MapperKind } from '@graphql-tools/utils';
function authDirective(schema) {
return mapSchema(schema, {
[MapperKind.OBJECT_FIELD]: (config) => {
const dir = getDirective(schema, config, 'auth')?.[0];
if (!dir) return;
const { requires } = dir;
const { resolve } = config;
config.resolve = (src, args, ctx, info) => {
if (!ctx.user) throw new GraphQLError('Unauthenticated', { extensions: { code: 'UNAUTHENTICATED' } });
if (requires === 'ADMIN' && ctx.user.role !== 'ADMIN') {
throw new GraphQLError('Forbidden', { extensions: { code: 'FORBIDDEN' } });
}
return resolve(src, args, ctx, info);
};
return config;
},
});
}
const schema = authDirective(makeExecutableSchema({ typeDefs, resolvers }));
const server = new ApolloServer({ schema });
// 5) DataLoader — batch + cache per request
import DataLoader from 'dataloader';
function createLoaders(db) {
return {
user: new DataLoader(async (ids) => {
const users = await db.users.findByIds(ids);
const map = new Map(users.map(u => [u.id, u]));
return ids.map(id => map.get(id)); // MUST return in same order
}),
postsByAuthor: new DataLoader(async (authorIds) => {
const posts = await db.posts.findByAuthors(authorIds);
const byAuthor = new Map(authorIds.map(id => [id, []]));
for (const post of posts) byAuthor.get(post.authorId).push(post);
return authorIds.map(id => byAuthor.get(id));
}),
};
}
// In resolvers:
resolvers.Post.author = (post, _, ctx) => ctx.loaders.user.load(post.authorId);
resolvers.User.posts = (user, _, ctx) => ctx.loaders.postsByAuthor.load(user.id);
// 6) Error handling
import { GraphQLError } from 'graphql';
resolvers.Query.user = async (_, { id }, ctx) => {
const user = await ctx.db.users.findById(id);
if (!user) {
throw new GraphQLError('User not found', {
extensions: { code: 'NOT_FOUND', http: { status: 404 } },
});
}
return user;
};
// Format errors before sending to client
const server = new ApolloServer({
typeDefs,
resolvers,
formatError: (formatted, error) => {
// Log full error server-side
log.error({ err: error });
// Strip stack from production
if (process.env.NODE_ENV === 'production') {
return { message: formatted.message, extensions: { code: formatted.extensions?.code } };
}
return formatted;
},
});
// 7) Caching — per-resolver TTL
import { ApolloServerPluginCacheControl } from '@apollo/server/plugin/cacheControl';
import { ApolloServerPluginResponseCache } from '@apollo/server-plugin-response-cache';
const typeDefs = /* GraphQL */ `
type Post @cacheControl(maxAge: 60) {
id: ID!
title: String!
body: String! @cacheControl(maxAge: 300)
}
type Query {
posts: [Post!]!
}
`;
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [
ApolloServerPluginCacheControl({ defaultMaxAge: 0 }),
ApolloServerPluginResponseCache(),
],
});
// 8) Subscriptions (WebSocket)
import { useServer } from 'graphql-ws/lib/use/ws';
import { WebSocketServer } from 'ws';
import { PubSub } from 'graphql-subscriptions';
const pubsub = new PubSub();
const resolvers2 = {
Subscription: {
postCreated: { subscribe: () => pubsub.asyncIterableIterator(['POST_CREATED']) },
},
Mutation: {
createPost: async (_, { input }, ctx) => {
const post = await ctx.db.posts.create(input);
pubsub.publish('POST_CREATED', { postCreated: post });
return post;
},
},
};
const wsServer = new WebSocketServer({ server: http_, path: '/graphql' });
const serverCleanup = useServer({ schema, context: createContext }, wsServer);
// Add to ApolloServer plugins:
plugins: [
ApolloServerPluginDrainHttpServer({ httpServer: http_ }),
{
async serverWillStart() {
return { async drainServer() { await serverCleanup.dispose(); } };
},
},
],
// 9) Federation — split schema across services
// apollo-router (Rust) or gateway (Node) routes queries to subgraphs.
// Each subgraph runs Apollo Server with @key directives.
import { buildSubgraphSchema } from '@apollo/subgraph';
const schema = buildSubgraphSchema({ typeDefs, resolvers });
const server = new ApolloServer({ schema });
// 10) Apollo Studio — observability
const server = new ApolloServer({
typeDefs,
resolvers,
introspection: true,
apollo: {
key: process.env.APOLLO_KEY, // from studio.apollographql.com
graphRef: 'my-graph@current',
},
});
// 11) Persisted queries — clients send hash, server looks up doc
import { createPersistedQueryLink } from '@apollo/client/link/persisted-queries';
import { sha256 } from 'crypto-hash';
const link = createPersistedQueryLink({ sha256 }).concat(httpLink);
// Server: ApolloServerPluginUsageReporting + persisted query plugin
// 12) Testing
import { ApolloServer } from '@apollo/server';
const testServer = new ApolloServer({ typeDefs, resolvers });
const result = await testServer.executeOperation({
query: 'query { users { id name } }',
}, {
contextValue: { user: testUser, db: mockDb, loaders: testLoaders },
});
expect(result.body.singleResult.data).toMatchObject({ users: [...] });
// 13) Common bugs
// • Forgetting DataLoader → N+1 queries
// • Resolver throws unhandled → 500; use GraphQLError with code
// • Schema directive without transformer → directive silently ignored
// • Subscription without WebSocket plugin → 'subscribe' is undefined
// • Cache-Control without ResponseCache plugin → no caching
// 14) Best practices
// ✅ Use code-first (Pothos) for TypeScript-first projects
// ✅ Always use DataLoader
// ✅ GraphQLError with extensions.code — clients branch on code
// ✅ Auth via schema directives or per-resolver wrappers
// ✅ Persisted queries in production (smaller payload, server controls allowed ops)
// ✅ Apollo Studio / GraphQL Hive for observability
// ✅ Disable introspection in production for closed APIs
Why it matters
Apollo Server + DataLoader + schema directives + Studio is the production-grade GraphQL stack. Use code-first (Pothos) for TS-heavy projects, persisted queries for performance and security in production.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
const server = new ApolloServer({ typeDefs, resolvers });
await startStandaloneServer(server, { listen: { port: 4000 } });
Try it Yourself »
Exercise
Server library by Apollo is…
import { ApolloServer } from '@apollo/
';
Six letters.
Discussion
Loading…