All posts
GraphQLMermaidAPISequence Diagrams

Document GraphQL APIs with Mermaid sequence diagrams

6 min readThe MermaidCreator team

GraphQL APIs are harder to document than REST endpoints because the query structure, variable flow, and resolver chains aren't obvious from a single URL. Mermaid sequence diagrams bring clarity — they show exactly what a client sends, what the server resolves, and how nested queries fan out to multiple sources.

Why GraphQL needs diagrams

REST APIs map to URLs and HTTP methods, so a quick reference is enough. GraphQL queries are nested, flexible, and often hide complexity:

  • A single query might resolve data from 5 different resolvers.
  • A mutation triggers side effects that the client can't see.
  • Subscriptions maintain open connections and push updates over time.
  • N+1 query problems are invisible in code but jump out of a diagram.

A sequence diagram makes the resolver chain explicit, showing team members — and future maintainers — exactly how their query flows through the server.

Basic query and resolver sequence

Here's a simple query fetching a user and their posts:

sequenceDiagram
    participant Client
    participant GraphQL as GraphQL Server
    participant DB as Database
    
    Client->>GraphQL: query { user(id: 1) { id name posts { title } } }
    GraphQL->>DB: SELECT * FROM users WHERE id = 1
    DB-->>GraphQL: { id: 1, name: "Alice" }
    GraphQL->>DB: SELECT * FROM posts WHERE user_id = 1
    DB-->>GraphQL: [{ title: "First" }, { title: "Second" }]
    GraphQL-->>Client: { user: { id: 1, name: "Alice", posts: [...] } }

The diagram shows:

  • The query structure sent by the client.
  • Each database call the server makes to resolve fields.
  • The return path back to the client.

This is the exact data flow — and it reveals if your resolvers are too chatty (multiple small queries instead of one joined query).

Mutations with side effects

Mutations are where diagrams shine. They show not just what the server does, but when side effects happen and whether they're observable:

sequenceDiagram
    participant Client
    participant GraphQL as GraphQL Server
    participant DB as Database
    participant Queue as Job Queue
    
    Client->>GraphQL: mutation { createOrder(items: [...]) { id total } }
    GraphQL->>DB: BEGIN TRANSACTION
    GraphQL->>DB: INSERT INTO orders (user_id, total, status) VALUES (...)
    DB-->>GraphQL: { id: 42 }
    GraphQL->>Queue: enqueue("send-confirmation-email", order_id: 42)
    Queue-->>GraphQL: ✓ queued
    GraphQL->>DB: COMMIT
    DB-->>GraphQL: ✓
    GraphQL-->>Client: { createOrder: { id: 42, total: 99.99 } }
    Note over Queue: Email sent asynchronously

This shows:

  • The order is persisted before the email job is queued.
  • The client sees the response before the email is sent.
  • Job queue failures don't block the mutation.

Subscription flow with live updates

Subscriptions maintain an open connection. Show them differently:

sequenceDiagram
    participant Client
    participant WebSocket as WebSocket Server
    participant GraphQL as GraphQL Server
    participant DB as Database
    participant Listener as Event Listener
    
    Client->>WebSocket: subscription { orderStatusChanged(orderId: 42) { status } }
    WebSocket-->>Client: ✓ subscription active
    
    rect rgb(200, 220, 255)
    Note over WebSocket,Listener: Initial state
    WebSocket->>DB: SELECT status FROM orders WHERE id = 42
    DB-->>WebSocket: "pending"
    WebSocket-->>Client: { orderStatusChanged: { status: "pending" } }
    end
    
    rect rgb(200, 255, 220)
    Note over DB,Listener: External update
    Listener->>DB: UPDATE orders SET status = 'shipped' WHERE id = 42
    Listener->>WebSocket: publish("order:42:updated")
    WebSocket->>GraphQL: Re-run subscription query
    GraphQL->>DB: SELECT status FROM orders WHERE id = 42
    DB-->>GraphQL: "shipped"
    WebSocket-->>Client: { orderStatusChanged: { status: "shipped" } }
    end
    
    Client->>WebSocket: unsubscribe()
    WebSocket-->>Client: ✓ closed

This makes clear:

  • The subscription sends an initial response.
  • Updates arrive asynchronously when the backend broadcasts changes.
  • Unsubscribe closes the connection cleanly.

Batching and DataLoader pattern

N+1 queries kill performance. The DataLoader pattern batches resolvers:

sequenceDiagram
    participant Client
    participant GraphQL as GraphQL Server
    participant DataLoader
    participant DB as Database
    
    Client->>GraphQL: query { users { id posts { title } } }
    
    rect rgb(255, 240, 200)
    Note over GraphQL,DataLoader: Resolve users
    GraphQL->>DB: SELECT * FROM users
    DB-->>GraphQL: [User 1, User 2, User 3]
    end
    
    rect rgb(240, 200, 255)
    Note over GraphQL,DataLoader: DataLoader batches post fetches
    GraphQL->>DataLoader: load(user_id: 1)
    GraphQL->>DataLoader: load(user_id: 2)
    GraphQL->>DataLoader: load(user_id: 3)
    DataLoader->>DB: SELECT * FROM posts WHERE user_id IN (1, 2, 3)
    DB-->>DataLoader: [Posts...]
    DataLoader-->>GraphQL: [User 1's posts]
    DataLoader-->>GraphQL: [User 2's posts]
    DataLoader-->>GraphQL: [User 3's posts]
    end
    
    GraphQL-->>Client: { users: [{ id, posts }, ...] }

The diagram shows:

  • User query resolves first (one SELECT).
  • Post resolver calls stack up in the DataLoader.
  • DataLoader batches all three into a single query (IN clause).
  • Results return in the correct order.

Without this pattern, you'd see 3 separate post queries (or 3×N for N users).

Error handling and retries

Show fallbacks and error paths explicitly:

sequenceDiagram
    participant Client
    participant GraphQL as GraphQL Server
    participant Cache
    participant API as External API
    
    Client->>GraphQL: query { externalData(id: 123) }
    
    GraphQL->>Cache: get("external_data:123")
    alt Cache hit
        Cache-->>GraphQL: { data: "cached value", stale: false }
    else Cache miss or stale
        Cache-->>GraphQL: null or stale entry
        GraphQL->>API: GET /api/data/123
        alt Success
            API-->>GraphQL: { data: "fresh value" }
            GraphQL->>Cache: set("external_data:123", value, ttl: 3600)
        else API timeout (retry)
            GraphQL->>API: GET /api/data/123 (retry 1)
            alt Still fails
                API--x GraphQL: connection timeout
                GraphQL-->>Client: { errors: [{ message: "Service unavailable" }] }
            else Success on retry
                API-->>GraphQL: { data: "fresh value" }
            end
        end
    end
    
    GraphQL-->>Client: { externalData: { data } }

This shows:

  • Cache check happens first.
  • On miss, the external API is called.
  • Retry logic is explicit and visible.
  • Timeout path leads to a graceful error response.

Tips for GraphQL sequence diagrams

  1. Name participants clearly: Use "GraphQL Server", "User Service", "Payment API" — don't just call them "Server".
  2. Show transactions: Wrap multi-step operations (BEGIN, COMMIT) to emphasize atomicity.
  3. Highlight async work: Use rect boxes to show background jobs, queues, and eventual-consistency flows.
  4. Batch queries explicitly: Use a "DataLoader" participant to show where N+1 problems are solved.
  5. Label each field: If a query resolves 10 fields from 8 different sources, document it — that's the diagram's job.
  6. Separate happy path from errors: Use alt blocks for success vs. fallback paths.

Try drawing your most complex mutation or subscription in the editor — you'll likely spot a resolver you didn't know was being called.

FAQ

Q: Should I diagram every query, or just complex ones?
A: Diagram any query where the resolver chain isn't obvious from code inspection. If a single field makes 3+ database calls or reaches an external API, diagram it.

Q: Can I embed these in our API docs?
A: Yes. Export as an image (or link to a MermaidCreator diagram) and embed in your README or API reference. Keep the Mermaid source in version control.

Q: How do I show a cached miss vs. hit in a diagram?
A: Use alt blocks (or dashed lines for "immediate return"). Label the return flow with ✓ from cache or fresh fetch.

Ready to diagram your API flows? Start in the playground and explore how Mermaid reveals resolver chains you didn't know you had.

Related posts