Part 3: Building a GraphQL API with TypeScript and Apollo Server

From Schema to Running Service

Part 2 designed the schema on paper. Now we implement it.

This part builds a production-style Apollo Server 4 application: Prisma 5 for database access, DataLoader to solve the N+1 query problem, Zod for input validation, and graphql-ws for subscriptions. The code is structured for a microservices context β€” this is one service among several that will federate in Part 6.

Project Structure

graphql-product-service/
β”œβ”€β”€ prisma/
β”‚   └── schema.prisma
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ context.ts          # Context type + factory
β”‚   β”œβ”€β”€ dataloaders.ts      # All DataLoader definitions
β”‚   β”œβ”€β”€ errors.ts           # Custom error classes
β”‚   β”œβ”€β”€ resolvers/
β”‚   β”‚   β”œβ”€β”€ index.ts        # Merge all resolvers
β”‚   β”‚   β”œβ”€β”€ product.ts      # Product query/mutation resolvers
β”‚   β”‚   β”œβ”€β”€ category.ts     # Category resolvers
β”‚   β”‚   └── subscription.ts # Subscription resolvers
β”‚   β”œβ”€β”€ schema.ts           # SDL typeDefs
β”‚   β”œβ”€β”€ validation.ts       # Zod schemas for inputs
β”‚   └── index.ts            # Server entry point
β”œβ”€β”€ package.json
└── tsconfig.json

Dependencies

Prisma Schema

Context

The context object is created per-request. I put database access, DataLoaders, and the authenticated user here.

DataLoaders: Solving the N+1 Problem

The N+1 query problem is the single most important performance issue in GraphQL. Consider:

Without DataLoader, fetching 20 products fires 1 query for products + 20 queries for their categories = 21 queries. DataLoader batches the category lookups into a single WHERE id IN (...) query.

Key points about DataLoader:

  • Each call to loader.load(key) returns a Promise

  • DataLoader batches all .load() calls from the same tick of the event loop into a single batch function call

  • The cache means repeated loads with the same key return the same Promise (per-request cache, not cross-request)

  • The context factory creates a new set of DataLoaders per request so the cache doesn't leak between requests

Resolvers

Product Resolvers

Category Resolvers

Subscriptions

Subscriptions use a pub/sub mechanism. Apollo Server v4 does not ship a built-in PubSub for production β€” the in-memory PubSub from graphql-subscriptions is only for development. For production I use Redis pub/sub via graphql-redis-subscriptions.

Publish an event from a mutation:

Merging Resolvers

Zod Input Validation

When Zod throws a ZodError, map it to UserInputError in an error-handling wrapper or in the resolver itself.

Custom Errors

Server Entry Point

What's Next

You now have a running Apollo Server with:

  • Prisma 5 database access

  • DataLoaders eliminating N+1 query problems

  • Cursor-based pagination

  • Zod input validation

  • Subscriptions via graphql-ws

  • Custom error classes with error codes

In Part 4, we consume this API β€” building a TypeScript client with Apollo Client and generated types, and a Python microservice that calls this GraphQL API using the gql client library.

Last updated