Document GraphQL APIs with Mermaid sequence diagrams
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 (
INclause). - 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
- Name participants clearly: Use "GraphQL Server", "User Service", "Payment API" — don't just call them "Server".
- Show transactions: Wrap multi-step operations (
BEGIN,COMMIT) to emphasize atomicity. - Highlight async work: Use
rectboxes to show background jobs, queues, and eventual-consistency flows. - Batch queries explicitly: Use a "DataLoader" participant to show where N+1 problems are solved.
- Label each field: If a query resolves 10 fields from 8 different sources, document it — that's the diagram's job.
- Separate happy path from errors: Use
altblocks 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
Visualize API request flows with Mermaid sequence diagrams
Use Mermaid sequence diagrams to document API interactions, request-response cycles, authentication flows, and error handling. Perfect for API documentation.
Mermaid sequence diagram actors and lifelines: messaging patterns
Master sequence diagram fundamentals — design interactions between services, APIs, and users with proper actor notation, lifelines, and message ordering.