In short
Time to build a real one: a bookstore GraphQL API with authors, books and reviews. You'll hit the classic problems, nested data, N+1 queries and messy errors, and fix each one yourself.
A GraphQL API for a real domain (e.g. a bookstore or blog) — practice for schema design, resolvers, and handling nested/related data efficiently.

Think of it like this
Running the library desk
A good librarian knows which books belong to which author, and which reviews belong to which book. Ask for 'Premchand's books with their reviews' and she brings it all in one trip. Your API plays librarian: one query, linked data, nothing extra.
Watch it happen
Build plan
1/6Write the SDL: Author, Book, Review, and a Query type with books and book(id).
The big ideas
What you need to know
A clean schema
Types for Author, Book and Review, non-null where data always exists, and input types for mutations.
No N+1 queries
Listing 20 books with authors should run 2 database queries, not 21. DataLoader makes that happen.
Mutations that validate
addReview should reject a rating outside 1 to 5 with a clear error, not a crash.
Paginated lists
books(first: 10, after: cursor) keeps big lists manageable, just like REST pagination.
See it in code
Walk through the code
1import DataLoader from "dataloader";2 3export function createLoaders() {4 return {5 author: new DataLoader(async (ids: readonly string[]) => {6 const rows = await db.authors.findMany({ where: { id: { in: [...ids] } } });7 const byId = new Map(rows.map((a) => [a.id, a]));8 return ids.map((id) => byId.get(id) ?? null);9 }),10 };11}12 13// In resolvers, with fresh loaders put on context for every request:14const resolvers = {15 Book: { author: (book, _args, ctx) => ctx.loaders.author.load(book.authorId) },16};1/3DataLoader collects every author id asked for in one tick and fetches them in a single query.
Create loaders per request inside your context function, so one user's cached data never leaks into another user's request.
Save yourself the pain
Common mistakes
Oops
Sharing one DataLoader instance across all requests.
Do this instead
Its cache would serve stale data, or another user's data. Create fresh loaders for every request.
Oops
Designing the schema as a copy of your database tables.
Do this instead
Design for what screens need. Hide join tables and internal columns behind clean types.
Your mission
First milestone: books and authors
Quick check
Twenty books are listed, each with its author. Without batching, how many database queries run?
Go deeper
- 1Design a schema covering at least two related types with nested queries.
- 2Implement a mutation alongside your queries, e.g. create a review.
- 3Add DataLoader to batch and cache resolver calls, avoiding N+1 queries.
More projects at this stop
Stuck on this topic? Ask an Insider
Get 1:1 guidance from people who've walked this exact path — on the InsideEdge app.