Building a GraphQL Server with Node.js
🏗️ Building a GraphQL Server with Node.js
Choosing a Server Library
Two common options: Apollo Server (batteries-included, the most widely adopted) and graphql-yoga (lighter, built closely on the spec's reference implementation). This chapter walks through Apollo Server, but every concept here — typeDefs, resolvers, context — transfers directly to graphql-yoga or any other spec-compliant server.
Basic Setup
npm install @apollo/server graphql
import { ApolloServer } from "@apollo/server"; import { startStandaloneServer } from "@apollo/server/standalone"; const typeDefs = `#graphql type Query { user(id: ID!): User } type User { id: ID! name: String! } `; const resolvers = { Query: { user: (parent, args, context) => context.userLoader.load(args.id), }, }; const server = new ApolloServer({ typeDefs, resolvers }); const { url } = await startStandaloneServer(server, { async context({ req }) { return { userLoader: createUserLoader() }; // Chapter 5 — fresh per request }, });
🧳 The Context Function, in Practice
This is exactly where Chapter 5's per-request DataLoader instances get created, and where authentication info will go in Chapter 8 — the context function runs once per incoming request and returns whatever object every resolver's third parameter will see.
Wiring Resolvers to a Real Data Source
The db.users.findById(...)-style pseudocode from earlier chapters becomes a real database client — the same mysql2 patterns from Node.js Course 2:
function createUserLoader(pool) { return new DataLoader(async (ids) => { const [rows] = await pool.query( "SELECT * FROM users WHERE id IN (?)", [ids] ); return ids.map(id => rows.find(r => r.id === id)); }); }
🔗 Attaching to Express
In a real deployment, GraphQL usually sits inside an existing Express app rather than standing alone — mounted at one path:
A Complete Minimal Server
import express from "express"; import { expressMiddleware } from "@apollo/server/express4"; const app = express(); const server = new ApolloServer({ typeDefs, resolvers }); await server.start(); app.use( "/graphql", // one endpoint — Chapter 1's theme, made concrete express.json(), expressMiddleware(server, { async context({ req }) => ({ userLoader: createUserLoader(pool) }), }) ); app.listen(4000);
Built-In Playground
Apollo Server ships a browser-based GraphiQL-style explorer during development — writing and running queries by hand against the live schema, with autocomplete powered by introspection from Chapter 2.
Splitting typeDefs as Schemas Grow
A schema doesn't have to live in one string — larger APIs typically split typeDefs across multiple files by domain (users, posts, comments), merged together at startup.
Where the Routing Actually Lives
REST
Routing is scattered across many app.get/post/put/delete calls, often spread across multiple route files.
GraphQL
One app.use("/graphql", ...) mount point — the schema and resolvers themselves are the real "routing," not Express routes.
💻 Coding Challenges
Challenge 1: Add a Second Loader to Context
Extend the Express-mounted server's context function to also create and return a postsByAuthorLoader (per Chapter 5's batching pattern), alongside the existing userLoader.
Goal: Practice building out a context function that carries multiple DataLoaders at once.
Challenge 2: Mount at a Custom Path With a Health Check
Modify the Express server example so GraphQL is mounted at /api/graphql instead of /graphql, and add a separate plain REST endpoint GET /health returning { status: "ok" }.
Goal: Practice combining a GraphQL endpoint with an ordinary REST route in the same Express app.
Challenge 3: Find the Bug in a Context Function
A developer writes const userLoader = createUserLoader(pool); at the TOP of the server file (outside any function), then references that same userLoader variable inside the context callback instead of calling createUserLoader(pool) there. Explain the bug this reintroduces.
Goal: Practice spotting Chapter 5's gotcha in realistic server-setup code, not just in isolated loader snippets.
This is Chapter 5's gotcha resurfacing in a very concrete place: it's tempting, while wiring up a real server, to create a DataLoader once near the top of the file — outside the context callback — to "avoid recreating it on every request." That's precisely the shared-instance mistake Chapter 5 warned about, just easier to accidentally write here because the context function is only a few lines and the loader creation looks like it could reasonably live outside it. Every DataLoader must be constructed inside the context callback itself, so a genuinely fresh instance — and fresh cache — exists for every single request.
🎯 What's Next
With a real server running, the next chapter covers what happens when things go wrong: Error Handling & Validation — GraphQL's error format compared to HTTP status codes, and custom error types.