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.

Incoming Query
query {
user(id: 1) { ← Click
name
posts {
title
}
}
}
Active Resolver & DB Call
Select a field on the left.

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. 1 Query to fetch users: SELECT * FROM users LIMIT 10;
  2. 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

Timeline DB Query Log
Total DB Queries
0
Total Latency
0ms

2. Query Cost Calculator

2
5
Estimated Complexity Score
10
SAFE

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

  1. 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).
  2. 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

Apollo Federation Architecture
Client
Sends 1 Query
Apollo Gateway
Query Planner & Composer
User Subgraph
Resolves `User`
Review Subgraph
Resolves `Review`

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:

  1. Bandwidth: Sending 2KB of query text for every request.
  2. Security: Malicious users can send deeply nested queries (DoS).

Solution: Persisted Queries.

  1. Build Time: Client compiles queries and hashes them (SHA-256).
    • query GetUser { ... }Hash: abc1234
  2. Runtime: Client sends only the hash.
    • GET /graphql?extensions={"persistedQuery":{"sha256Hash":"abc1234"}}
  3. 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.