Building a GraphQL Server with Node.js

GraphQL — Building a GraphQL Server with Node.js
GraphQL
Chapter 6 · Building a GraphQL Server with Node.js

🏗️ Building a GraphQL Server with Node.js

Every chapter so far has been schema, resolvers, and DataLoader as concepts and snippets. This chapter wires all of it into an actual running server, using the same Node.js and Express foundation already covered in this curriculum.

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.

→ Solution

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.

→ Solution

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.

→ Solution

⚠️ Gotcha: Hoisting Loader Creation Out of the Context Function

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.