Skip to main content

How to Build a GraphQL Server

Build a GraphQL server with a typed schema and resolvers, eliminate the N+1 problem with DataLoader batching, return coded errors, and protect the API with depth and cost limits.

Difficulty
Intermediate
Duration
50 minutes
Steps
6

What and why

GraphQL exposes a typed schema where clients request exactly the fields they need in a single round trip, avoiding over-fetching and under-fetching common with REST. The trade-off is server-side complexity: you must handle batching, errors, and query cost yourself.

Prerequisites

  • Node.js installed.
  • A data source (a database or an API).
  • Basic JavaScript.

Steps

1. Define the schema

type Author { id: ID!, name: String!, books: [Book!]! }
type Book { id: ID!, title: String!, author: Author! }
type Query { authors: [Author!]!, book(id: ID!): Book }

The schema is the contract; types and relationships are explicit.

2. Write resolvers

Resolvers fetch data for each field:

const resolvers = {
  Query: {
    authors: () => db.authors.findAll(),
    book: (_, { id }) => db.books.findById(id),
  },
  Author: { books: (author) => db.books.findByAuthor(author.id) },
};

3. Run the server

const { ApolloServer } = require('@apollo/server');
const { startStandaloneServer } = require('@apollo/server/standalone');
const server = new ApolloServer({ typeDefs, resolvers });
const { url } = await startStandaloneServer(server, { listen: { port: 4000 } });

4. Batch with DataLoader

Resolving books per author causes one query per author (the N+1 problem). DataLoader batches and caches lookups within a request:

const booksLoader = new DataLoader(ids => batchBooksByAuthor(ids));
// in resolver:
Author: { books: (author) => booksLoader.load(author.id) }

5. Handle errors

Return typed errors with codes rather than leaking internals. Use the extensions.code field (for example UNAUTHENTICATED, BAD_USER_INPUT) so clients can react programmatically, and mask stack traces in production.

6. Limit query depth and cost

A single GraphQL query can be arbitrarily deep and expensive. Add depth limiting and a cost analysis plugin to reject abusive queries, and disable introspection in production if the API is private.

Verification

  • The schema loads and the playground lists types.
  • A nested query returns related data in one request.
  • With DataLoader, related lookups collapse into batched queries.
  • An overly deep query is rejected.

Next Steps

Add authentication via context, paginate list fields with cursors (Relay connections), add persisted queries to cap the allowed query set, and consider federation if multiple services contribute to one graph.

Prerequisites

  • Node.js installed
  • A data source
  • Basic JavaScript knowledge

Steps

  • 1
    Define the schema
  • 2
    Write resolvers
  • 3
    Run the server
  • 4
    Batch with DataLoader
  • 5
    Handle errors
  • 6
    Limit query depth and cost

Category

API Design