GraphQL

A query language for APIs that allows clients to request exactly the data they need.

GraphQL is a query language for APIs that lets clients request exactly the data they need. Unlike REST where the server defines what data each endpoint returns, GraphQL puts the client in control. You write a query describing what you want, and the server returns exactly that - nothing more, nothing less.

GraphQL vs REST

AspectRESTGraphQL
EndpointsMultiple (/users, /posts)Single (/graphql)
Data fetchingFixed response per endpointClient specifies fields
Over-fetchingCommon - get all fieldsNone - get only requested
Under-fetchingCommon - need multiple callsNone - get related data in one query
VersioningURL or header (/v1/, /v2/)Usually not needed
CachingHTTP caching built-inMore complex, needs tools

Core Concepts

Schema: Defines your API's types and their relationships. It's a contract between client and server.

Queries: Read operations. Ask for data, get it back.

Mutations: Write operations. Create, update, or delete data.

Subscriptions: Real-time updates. Server pushes data when something changes.

Resolvers: Functions that fetch the actual data for each field in your schema.

The Schema

The schema defines what's possible in your API:

Types: Define the shape of data

  • Scalar types: String, Int, Float, Boolean, ID
  • Object types: Custom types with fields
  • Lists: [User] - array of users
  • Non-null: String! - can't be null

Root types:

  • Query: Entry point for reads
  • Mutation: Entry point for writes
  • Subscription: Entry point for real-time

Why GraphQL?

Solve over-fetching: REST GET /users/1 returns everything about the user. GraphQL returns only what you ask for.

Solve under-fetching: REST: call /users/1, then /users/1/posts, then /posts/1/comments. GraphQL: one query gets user with posts and comments.

Strongly typed: Schema acts as documentation. Tools provide autocomplete and validation.

Evolve without versions: Add fields without breaking clients. Deprecate old fields gradually.

Single request: Mobile apps on slow networks benefit from fewer roundtrips.

When to Use GraphQL

Multiple clients with different needs: Mobile app needs minimal data, web dashboard needs everything. GraphQL lets each request exactly what it needs.

Complex, interconnected data: Social networks, e-commerce with products/reviews/users - GraphQL handles nested relationships elegantly.

Rapid frontend iteration: Frontend can request new fields without backend changes (if schema allows).

API aggregation: GraphQL can sit in front of multiple services, providing unified interface.

When NOT to Use GraphQL

Simple CRUD APIs: If you just fetch and update single resources, REST is simpler.

File uploads: GraphQL handles this poorly. Use REST for files.

Heavy caching needs: HTTP caching is harder with GraphQL since all requests go to one endpoint.

Real-time only: If you only need server push without queries, SSE or WebSocket might be simpler.

N+1 Problem in GraphQL

GraphQL is particularly vulnerable to N+1:

Query asks for 100 users with their posts. Without optimization:

  • 1 query for users
  • 100 queries for posts (one per user)

Solution: DataLoader Batches requests within a single tick. Collects all user IDs, makes one query for all posts.

Common Mistakes

1. Not using DataLoader: Every nested field causes N+1 queries. DataLoader is mandatory, not optional.

2. Exposing database schema directly: GraphQL schema should match client needs, not database tables.

3. No depth limiting: Malicious query: user { friends { friends { friends { ... } } } } can crash your server. Limit query depth.

4. Ignoring errors: GraphQL returns 200 even with errors. Check the errors array, not just status code.

5. Not documenting: Just because schema is self-documenting doesn't mean it's well-documented. Add descriptions.

Best Practices

Design schema for clients: Don't mirror your database. Design for how clients use data.

Use fragments for reusability: Define common field sets once, reuse in queries.

Implement pagination: Never return unbounded lists. Use cursor-based pagination.

Add query complexity limits: Prevent expensive queries from overwhelming your server.

Use persisted queries in production: Client sends hash instead of full query. Smaller payloads, prevents arbitrary queries.

Code Examples

GraphQL Schema and Queries

# Schema Definition
type User {
  id: ID!
  name: String!
  email: String!
  posts: [Post!]!
  createdAt: DateTime!
}

type Post {
  id: ID!
  title: String!
  content: String!
  author: User!
  comments: [Comment!]!
}

type Query {
  user(id: ID!): User
  users(limit: Int, offset: Int): [User!]!
  post(id: ID!): Post
}

type Mutation {
  createUser(input: CreateUserInput!): User!
  updateUser(id: ID!, input: UpdateUserInput!): User!
  deleteUser(id: ID!): Boolean!
}

# Query Example - Get exactly what you need
query GetUserWithPosts {
  user(id: "123") {
    name
    email
    posts {
      title
      comments {
        text
        author { name }
      }
    }
  }
}

# Mutation Example
mutation CreatePost {
  createPost(input: {
    title: "GraphQL Guide"
    content: "Learn GraphQL..."
    authorId: "123"
  }) {
    id
    title
  }
}