Capstone: Build a Production-Ready Blog API

Advanced
16 min

Capstone: Build a Production-Ready Blog API

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.

Project Setup

bash
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-resolvers

Suggested layout:

bash
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 tests

better-sqlite3 keeps the database in one file; swapping in PostgreSQL later only touches db.js.

The Schema

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.

Server Skeleton

javascript
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"));

Milestones

| 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.

Acceptance Checklist

  • Every list query is paginated and first is clamped to 50.
  • Unauthenticated calls to createPost return UNAUTHENTICATED; another user's email resolves to null.
  • { posts { edges { node { author { name } } } } } issues exactly two SQL queries.
  • Internal errors are masked with formatError; expected errors arrive in CreatePostPayload.errors.

Stretch Goals

  • Add a React or Next.js frontend with Apollo Client and subscribeToMore for live comments.
  • Split users and posts into two subgraphs behind Apollo Router.
Quick Quiz
Question 1 of 2

Why does the acceptance checklist require exactly two SQL queries for a posts-with-authors query?

Key Takeaways

  • A complete service combines schema design, DataLoader, pagination, auth, error handling, subscriptions and tests.
  • The SDL file is the source of truth; codegen and tests keep resolvers honest against it.
  • Build in milestones, verifying each in Apollo Sandbox and with in-process tests.
  • Security defaults (depth limit, masked errors, introspection off in production) belong in the skeleton from day one.

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.

Capstone: Build a Production-Ready Blog API - GraphQL | CodeYourCraft | CodeYourCraft