Overview
GraphQL is a query language and runtime for APIs. The server publishes a typed schema, and each client sends a query describing exactly the fields it needs, possibly across related objects, in a single request. The server resolves each field with resolver functions and returns JSON in the same shape as the query.
It solves REST's over-fetching and under-fetching, which matters most for complex UIs and mobile apps. The costs are real: caching is harder (usually one POST endpoint), naive resolvers cause N+1 database queries, and clients can craft expensive queries, so production GraphQL needs batching, query cost limits, and persisted queries.
REST is a set menu: each dish comes with fixed sides whether you want them or not, and you may need to order from several menus. GraphQL lets you tell the waiter exactly what you want on one plate, taken from several kitchens.
When to use it
- Many different clients need different slices of the same data.
- Screens combine data from several services in one view.
- Mobile clients on slow networks where round trips are costly.
- Rapid front-end iteration without new endpoints for every screen.
Where it shows up in interviews
Recognize it when: one screen needs data from users, posts, and comments services.
- Design Facebook's news feed API
- Design a mobile app backend
Recognize it when: many teams own parts of one graph.
- Design a unified API for 50 microservices
Where it is used in real software
GraphQL was created at Facebook in 2012 for the mobile news feed, where many round trips on slow networks were unacceptable.
Offers a public GraphQL API alongside REST, with a rate limit based on query cost points.
Lets teams own subgraphs that a gateway composes into one supergraph (used by Netflix, Expedia).
Key terms
- Schema
- Typed description of objects, fields, queries, and mutations.
- Resolver
- A function that returns the value for one field.
- Mutation
- An operation that changes data.
- Subscription
- Real-time updates pushed over WebSockets.
- DataLoader
- Batches and caches loads within a request to avoid N+1 queries.
How it works, step by step
- 1Client sends a query
POST /graphql with the query text (or a persisted query ID) and variables.
- 2Server parses and validates
Checks syntax and types against the schema; rejects unknown fields.
- 3Cost analysis
Estimate depth and complexity; reject queries above the limit.
- 4Resolve fields
Resolvers run for each field; DataLoader batches related lookups.
- 5Return shaped JSON
The response mirrors the query; partial errors appear in an errors array.
The N+1 problem and DataLoader
Query: 50 posts, each with its author
| Approach | Database queries | Why |
|---|---|---|
| Naive resolvers | 1 + 50 = 51 | author resolver runs once per post |
| DataLoader batching | 1 + 1 = 2 | all author IDs collected and loaded with one IN query |
| Join in the posts resolver | 1 | but couples resolvers to query shape |
NOWApproach: Naive resolvers | Database queries: 1 + 50 = 51 | Why: author resolver runs once per post
DataLoader is the standard fix: it waits until the current tick, collects all requested keys, and issues one batched query.
Implementation
import DataLoader from "dataloader"; const typeDefs = `#graphql type User { id: ID!, name: String! } type Post { id: ID!, title: String!, author: User! } type Query { feed(first: Int = 20, after: String): [Post!]! } type Mutation { createPost(title: String!): Post! }`; // One loader per request so caching never leaks between usersfunction createLoaders(db: Db) { return { userById: new DataLoader<string, User>(async (ids) => { const rows = await db.users.findMany({ where: { id: { in: [...ids] } } }); const byId = new Map(rows.map((u) => [u.id, u])); return ids.map((id) => byId.get(id)!); }), };} const resolvers = { Query: { feed: (_: unknown, args: { first: number; after?: string }, ctx: Ctx) => ctx.db.posts.page(args), }, Post: { author: (post: Post, _: unknown, ctx: Ctx) => ctx.loaders.userById.load(post.authorId), // batched }, Mutation: { createPost: (_: unknown, { title }: { title: string }, ctx: Ctx) => ctx.db.posts.create({ title, authorId: ctx.user.id }), },};Complexity and performance
One query for nested data.
Fix with DataLoader.
Needs persisted queries or client caches.
Trade-offs
Clients can ask for anything in the schema, so you must limit depth and cost, and consider persisted queries for public clients.
CDN caching is easy for REST GET URLs; GraphQL relies on client-side normalized caches (Apollo, Relay) or GET with persisted query hashes.
Variants and related techniques
Multiple subgraphs composed by a gateway, each owned by a team.
Clients send a hash of an approved query, enabling CDN caching and blocking arbitrary queries.
Common mistakes
- N+1 resolver queries.
Fix: Use DataLoader per request.
- Unbounded query depth.
Fix: Enforce depth and complexity limits and timeouts.
- Authorization only at the endpoint.
Fix: Check permissions in resolvers or the data layer, per field and object.
Interview questions
When would you choose GraphQL over REST?
When many clients need different shapes of related data, especially mobile clients where round trips are expensive, or when aggregating many backend services for a UI.
How do you prevent abusive GraphQL queries?
Depth limits, query cost analysis with budgets per client, pagination limits, timeouts, and persisted queries for first-party clients.
Practice problems
| Problem | Difficulty | What it trains |
|---|---|---|
| Model a schema for a blog with users, posts, comments | Easy | Types and relations. |
| Fix an N+1 problem in a feed resolver | Medium | DataLoader. |
| Design a federated graph for an e-commerce company | Hard | Ownership and gateway. |