
How GraphQL Works — Schemas, Queries, and the N+1 Problem
GraphQL is a query language for APIs. Facebook created it in 2012 and open-sourced it in 2015 to solve a specific problem: mobile clients needed different data than web clients, and REST endpoints either returned too much data (over-fetching) or required too many requests (under-fetching).
With GraphQL, the client describes exactly what it needs, and the server returns exactly that — nothing more, nothing less, in a single request.
The Problem GraphQL Solves
Imagine a social media profile page. You need the user's name, their last 5 posts, and each post's comment count. With REST:
GET /users/42— returns the user (includes 20 fields you don't need on mobile)GET /users/42/posts?limit=5— returns the postsGET /posts/101/comments/count,GET /posts/102/comments/count, ... — one request per post
That's 7 HTTP round trips. On a 200ms mobile connection, that's 1.4 seconds of latency just from the network — before the server even does any work.
With GraphQL, it's one request:
query {
user(id: 42) {
name
posts(limit: 5) {
title
commentCount
}
}
}
One round trip. Only the fields you asked for. The response mirrors the shape of the query.
The Schema
Every GraphQL API starts with a schema — a typed definition of everything the API can do. The schema defines types, their fields, and how they relate:
type User {
id: ID!
name: String!
email: String!
posts(limit: Int): [Post!]!
}
type Post {
id: ID!
title: String!
body: String!
author: User!
commentCount: Int!
}
type Query {
user(id: ID!): User
posts(limit: Int, offset: Int): [Post!]!
}
type Mutation {
createPost(title: String!, body: String!): Post!
deletePost(id: ID!): Boolean!
}
type Subscription {
postCreated: Post!
}
Three root types define what the API can do:
- Query — read data (like GET in REST)
- Mutation — write data (like POST/PUT/DELETE in REST)
- Subscription — real-time updates pushed from the server (typically over WebSockets)
The ! means non-nullable. [Post!]! means a non-null list of non-null posts. The schema is a contract between client and server — clients know exactly what fields exist and what types they return.
Resolvers — How Fields Are Fetched
Each field in the schema has a resolver — a function that returns the data for that field. When a query arrives, the GraphQL engine walks the query tree and calls the resolver for each field:
const resolvers = {
Query: {
user: (_, { id }) => db.users.findById(id),
},
User: {
posts: (user, { limit }) => db.posts.findByAuthor(user.id, limit),
},
Post: {
commentCount: (post) => db.comments.countByPost(post.id),
},
};
The engine calls Query.user first. The result is passed to User.posts as the parent argument. Each post is passed to Post.commentCount. Resolvers compose — each one fetches just the data for its own field.
REST Multiple Requests vs GraphQL Single Query
The N+1 Problem
The composable resolver model has a trap. If the query asks for 50 users and each user's posts, the engine calls Query.users once (1 query), then User.posts once per user (50 queries). That's 51 database queries for one GraphQL request — the N+1 problem.
It gets worse. If each post also resolves its author, comment count, and tags, a single GraphQL query can trigger hundreds of database queries.
DataLoader is the standard solution. Instead of fetching one user's posts immediately, the resolver registers the user ID. At the end of the current execution tick, DataLoader collects all registered IDs and makes a single batched query:
// Without DataLoader: 50 queries
// SELECT * FROM posts WHERE author_id = 1
// SELECT * FROM posts WHERE author_id = 2
// ...
// With DataLoader: 1 query
// SELECT * FROM posts WHERE author_id IN (1, 2, 3, ..., 50)
DataLoader is not optional. Any production GraphQL server without batched data loading will hit database performance problems immediately.
When GraphQL Is Better Than REST
GraphQL wins when:
- Clients need different views of the same data (mobile vs web vs watch)
- The data graph is deeply nested (users → posts → comments → authors)
- You want a single endpoint instead of dozens of REST routes
- Strong typing and schema introspection matter (tooling, code generation)
REST wins when:
- The API is simple CRUD with few relationships
- HTTP caching matters (GET requests are cacheable by default; GraphQL POST requests are not)
- You need file uploads (GraphQL has no native file upload spec)
- The team is small and REST's simplicity reduces cognitive overhead
Common Pitfalls
Query complexity attacks. A malicious client can send deeply nested queries that explode server resources. Production GraphQL servers must enforce query depth limits, complexity scoring, and timeouts.
No HTTP caching. All GraphQL requests are POST to /graphql. HTTP caches (CDNs, browser cache) don't work by default. You need application-level caching (persisted queries, response caching by query hash).
Schema evolution. GraphQL doesn't have versioning — fields are deprecated, not removed. This requires discipline. A @deprecated directive signals intent, but removing a field breaks any client still using it.
GraphQL Over the Wire
GraphQL is transport-agnostic, but in practice it runs over HTTP. Queries are sent as POST requests with a JSON body containing the query string. Responses are JSON with a data field (and optionally an errors field). Subscriptions typically use WebSockets or Server-Sent Events for the persistent connection.
Next Steps
- How gRPC Works — binary serialization and HTTP/2 streaming for internal services.
- How REST Works — the simpler alternative for resource-based APIs.
- How API Authentication Works — securing GraphQL endpoints.