iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

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/ ';

Test yourself

Q1. Apollo Server is a…
Q2. Resolvers map to…
Q3. Per-request goodies come from…

Discussion

Loading…