GraphQL Basics & Performance
[!TIP] Interview Tip: GraphQL is not a “silver bullet”. It solves over-fetching but introduces complexity (Caching, Rate Limiting, N+1 Problem). Be prepared to discuss these trade-offs.
GraphQL is a query language for your API, and a server-side runtime for executing queries by using a type system you define for your data. It was developed by Facebook in 2012 and open-sourced in 2015.
Unlike REST, where you hit multiple endpoints (/users, /users/1/posts), in GraphQL you hit a single endpoint (usually /graphql) and ask for exactly what you need.
1. Core Concepts
1.1 Schema (SDL)
The contract between client and server. It uses the SDL (Schema Definition Language).
type User {
id: ID!
name: String!
posts: [Post]
}
type Post {
id: ID!
title: String!
}
type Query {
getUser(id: ID!): User
}
1.2 Resolvers
The functions that actually fetch the data (from DB, Microservice, or 3rd party API). Resolvers are where the “magic” happens. They map fields to data.
const resolvers = {
Query: {
// Top-level resolver
getUser: (parent, args) => db.users.findById(args.id),
},
User: {
// Nested resolver for 'posts' field
posts: (user) => db.posts.findAll({ authorId: user.id }),
}
};
Interactive Visualizer: Resolver Execution Flow
Click on a field in the Query to see which Resolver executes and what SQL it triggers.
2. REST vs. GraphQL
| Feature | REST | GraphQL |
|---|---|---|
| Endpoints | Multiple (/users, /posts) |
Single (/graphql) |
| Data Fetching | Fixed structure (Over/Under-fetching) | Client defines structure (Exact fetching) |
| Versioning | v1, v2 | Deprecation fields (Evolutionary) |
| Caching | Easy (HTTP Caching) | Hard (Application-level caching required) |
| Error Handling | HTTP Status Codes | 200 OK with errors array in JSON |
The Problem with REST: Over-fetching
You need a user’s name. You call GET /users/1. The server returns:
{
"id": 1,
"name": "Alice",
"address": "...",
"preferences": "...",
"history": "..."
}
You wasted bandwidth downloading data you didn’t need.
The Solution: GraphQL
query {
user(id: 1) {
name
}
}
Response:
{ "data": { "user": { "name": "Alice" } } }
3. The N+1 Problem (Critical)
This is the most common performance pitfall in GraphQL.
Scenario: You want to fetch 10 users and their last post.
query {
users {
name
lastPost { title }
}
}
Execution Flow:
- 1 Query to fetch users:
SELECT * FROM users LIMIT 10; - N Queries (10) to fetch posts for each user:
SELECT * FROM posts WHERE user_id = ?;
Total Queries: 1 + N (11 queries). If you fetch 1000 users, that’s 1001 queries. This kills the database.
3.1 The Solution: DataLoader (Batching)
Instead of executing the post query immediately, we wait a few milliseconds (next tick), collect all user IDs, and execute one batch query.
SELECT * FROM posts WHERE user_id IN (1, 2, 3, ... 10);
Total Queries: 2 (Regardless of N).
Interactive Visualizer: N+1 Simulator & Query Cost
Visualize the difference between Naive execution (Sequential DB Hits) and Optimized execution (DataLoader Batching). Use the DB Query Log tab to see exactly what queries are being executed.
1. Database Query Monitor
2. Query Cost Calculator
4. Case Study: GitHub GraphQL API & Resource Limits
GitHub operates one of the world’s largest public GraphQL APIs (https://api.github.com/graphql).
They faced a massive problem: Complexity.
In REST, GET /repos returns a fixed cost.
In GraphQL, a user can ask for:
query {
viewer {
repositories(first: 100) {
issues(first: 100) {
comments(first: 100) {
body
}
}
}
}
}
If executed, this returns 1,000,000 nodes (100 * 100 * 100). One request could take down the DB.
GitHub’s Solution: Node Limit & Rate Limiting
- Node Limit: GitHub calculates the potential number of nodes in your query.
- Limit: 500,000 nodes per call.
- If your query could return more, it is rejected before execution (Static Analysis).
- Rate Limiting (Points):
- Instead of “Requests per Hour”, they use Points per Hour (5,000 points).
- Cost =
(Nodes + 5). - Simple queries cost 1 point. Complex queries cost 100 points.
Takeaway: When designing public GraphQL APIs, you MUST implement Query Cost Analysis.
5. Scaling GraphQL: Federation
When your organization grows, a single monolithic GraphQL server becomes a bottleneck.
Apollo Federation (The Modern Standard)
A declarative approach where you define a Supergraph.
- Subgraphs: Each microservice (Users, Reviews) defines its own schema and how it relates to others (e.g.,
extend type User). - Gateway: Automatically composes the Supergraph. It is “dumb” logic-wise; it just queries the subgraphs based on the plan.
Federation Architecture Diagram
6. Schema Design Best Practices
Designing a GraphQL schema is an art. It’s not just “Exposing your DB”.
6.1 User-Centric, Not DB-Centric
Don’t just mirror your SQL tables.
- Bad:
getUser(id: 1) { database_column_first_name } - Good:
getUser(id: 1) { firstName }
6.2 Use Specific Mutations
Avoid generic “Update” mutations with 50 optional fields.
- Bad:
updateUser(input: { id: 1, email: "...", status: "..." }) - Good:
changeUserEmail(userId: 1, newEmail: "...")banUser(userId: 1)
6.3 Pagination everywhere
If a field returns a list (Array), always paginate it from Day 1. You never know when user.friends will grow from 5 to 5,000.
7. Persisted Queries
In a standard GraphQL request, the client sends the entire query string (which can be huge) to the server. This has two problems:
- Bandwidth: Sending 2KB of query text for every request.
- Security: Malicious users can send deeply nested queries (DoS).
Solution: Persisted Queries.
- Build Time: Client compiles queries and hashes them (SHA-256).
query GetUser { ... }→Hash: abc1234
- Runtime: Client sends only the hash.
GET /graphql?extensions={"persistedQuery":{"sha256Hash":"abc1234"}}
- Server: Looks up the hash. If found, executes it. If not, asks for the full query once, caches it, and uses the hash next time.
Benefits:
- Performance: Tiny payloads.
- Security: You can “Lock” the server to ONLY accept known hashes in production (No more arbitrary queries!).
8. Summary
- Use GraphQL for complex data requirements (e.g., Mobile Apps, Dashboards) to avoid over-fetching.
- Watch out for the N+1 Problem; use DataLoader.
- Implement Depth Limits and Query Cost Analysis (like GitHub) to prevent DoS attacks.
- Use Persisted Queries to enable CDN caching and improve security.
Next, how do we handle Real-Time updates? Polling vs WebSockets? Check out Polling vs Push.