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.
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
- 1Define the schema
- 2Write resolvers
- 3Run the server
- 4Batch with DataLoader
- 5Handle errors
- 6Limit query depth and cost