Connecting types is GraphQL’s superpower and its classic trap. Resolve relationships across types, then fix the N+1 query explosion with batching via DataLoader.
Why: types connect through fields whose resolvers fetch related data. Here a Book has an author field that looks up the Author by the book's authorId. The client can now traverse from book to author in one query — the graph in "GraphQL".
const resolvers = {
Query: {
books: () => db.books.findAll(),
},
Book: {
// runs once per book to fetch its author
author: (book) => db.authors.findById(book.authorId),
},
}Why: that innocent author resolver hides a trap. Query 100 books and their authors, and GraphQL runs 1 query for the books plus 100 separate queries for authors — the "N+1 problem". It is the number-one GraphQL performance bug, and it appears the moment you resolve a list's relationships.
query { books { author { name } } } with 100 books:
1 query ─ SELECT * FROM books (the "1")
100 queries ─ SELECT * FROM authors WHERE id = ? (the "N", once per book)
└─▶ 101 database round-trips for one requestWhy: DataLoader fixes N+1 by collecting all the ids requested during one tick and fetching them in a single batched query. You give it a batch function that takes many ids and returns many results in order. The 100 author lookups collapse into one.
import DataLoader from 'dataloader'
// Batch function: many ids -> many authors, in the same order
const authorLoader = new DataLoader(async (ids) => {
const authors = await db.authors.findByIds(ids)
return ids.map((id) => authors.find((a) => a.id === id))
})
const resolvers = {
Book: {
author: (book) => authorLoader.load(book.authorId), // batched
},
}Why: a DataLoader must be created fresh per request (its cache should not leak between users). Create it in the context function, then resolvers read it from context. This is the standard pattern — per-request loaders, shared through context.
const server = new ApolloServer({ typeDefs, resolvers })
await startStandaloneServer(server, {
context: async () => ({
loaders: {
author: makeAuthorLoader(), // a new loader each request
},
}),
})
// In a resolver: (book, _args, ctx) => ctx.loaders.author.load(book.authorId)