This final chapter ties the course together: a blog API with users, posts, comments and live updates that applies a technique from nearly every lesson. It gives you the schema, the server skeleton and a milestone plan.
mkdir blog-api && cd blog-api && npm init -y
npm install @apollo/server @as-integrations/express5 express graphql @graphql-tools/schema \
graphql-scalars dataloader jsonwebtoken bcryptjs better-sqlite3 graphql-ws ws graphql-subscriptions graphql-depth-limit
npm install -D vitest @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-resolversSuggested layout:
src/
schema.graphql # SDL, the single source of truth
db.js # better-sqlite3 tables and query helpers
loaders.js # DataLoader factories (userById, commentsByPostId)
context.js # JWT verification, db, loaders, pubsub
resolvers/ # query.js, mutation.js, post.js, user.js, subscription.js
server.js # Express + Apollo + graphql-ws wiring
tests/ # executeOperation testsbetter-sqlite3 keeps the database in one file; swapping in PostgreSQL later only touches db.js.
The schema in the sample above is the contract to implement. It draws on a custom DateTime scalar, a Relay-style PostConnection, an input type with a UserError payload, a nullable email only the owner may see, and a commentAdded subscription.
import express from "express";
import { createServer } from "node:http";
import { ApolloServer } from "@apollo/server";
import { expressMiddleware } from "@as-integrations/express5";
import { makeExecutableSchema } from "@graphql-tools/schema";
import { WebSocketServer } from "ws";
import { useServer } from "graphql-ws/use/ws";
import depthLimit from "graphql-depth-limit";
import { typeDefs, resolvers } from "./schema.js";
import { buildContext, buildWsContext } from "./context.js";
const schema = makeExecutableSchema({ typeDefs, resolvers });
const app = express();
const httpServer = createServer(app);
const wsServer = new WebSocketServer({ server: httpServer, path: "/graphql" });
const wsCleanup = useServer({ schema, context: (ctx) => buildWsContext(ctx.connectionParams) }, wsServer);
const server = new ApolloServer({
schema,
introspection: process.env.NODE_ENV !== "production",
validationRules: [depthLimit(8)],
plugins: [{ async serverWillStart() { return { async drainServer() { await wsCleanup.dispose(); } }; } }],
});
await server.start();
app.use("/graphql", express.json(), expressMiddleware(server, { context: buildContext }));
httpServer.listen(4000, () => console.log("API ready at http://localhost:4000/graphql"));| Milestone | What to implement | Chapter to revisit |
|---|---|---|
| 1. Read-only API | posts, post, Post.author with DataLoader | Resolvers, DataLoader |
| 2. Pagination | Cursor-based posts(first, after) with pageInfo | Cursor Pagination |
| 3. Accounts | register and login with bcrypt and JWT; me | Authentication |
| 4. Writing | createPost with payload errors; addComment | Input Types, Error Handling |
| 5. Real time | commentAdded over graphql-ws, filtered by postId | Subscriptions |
| 6. Quality | executeOperation tests, codegen types | Testing, Codegen |
Work through them in order; a milestone is done when its operations run in Apollo Sandbox and npx vitest run passes.
first is clamped to 50.createPost return UNAUTHENTICATED; another user's email resolves to null.{ posts { edges { node { author { name } } } } } issues exactly two SQL queries.formatError; expected errors arrive in CreatePostPayload.errors.subscribeToMore for live comments.users and posts into two subgraphs behind Apollo Router.Why does the acceptance checklist require exactly two SQL queries for a posts-with-authors query?
What to learn next: deepen the data layer with the MongoDB course, build the frontend with the Next.js course, and practise deployment with Docker.