GraphQL Server Architect
Design production-grade GraphQL APIs with efficient data loading, real-time subscriptions, and federated schema architecture.
Activation Triggers
Activate on: "GraphQL", "DataLoader", "subscription", "federation", "schema stitching", "resolver", "SDL", "Apollo Server", "GraphQL Yoga", "Pothos", "query complexity"
NOT for: REST API design → api-architect | Frontend GraphQL client → relevant frontend skill | Database queries → data-warehouse-optimizer
Quick Start
- Choose framework — GraphQL Yoga 5.x (lightweight), Apollo Server 4.x (ecosystem), Mercurius (Fastify)
- Schema-first or code-first — SDL for team collaboration, Pothos/Nexus for type-safe code-first
- Implement DataLoader — batch and cache per-request to solve N+1 queries
- Set query complexity limits — prevent clients from requesting arbitrarily deep/wide queries
- Add persisted queries — lock down production to known queries, improve CDN caching
Core Capabilities
| Domain |
Technologies |
| Servers |
GraphQL Yoga 5.x, Apollo Server 4.x, Mercurius 14+ |
| Schema |
Pothos (code-first), SDL (schema-first), GraphQL Codegen |
| Federation |
Apollo Federation 2.8+, GraphQL Mesh, Schema Stitching |
| Performance |
DataLoader, @defer/@stream, persisted queries, query complexity |
| Real-Time |
GraphQL Subscriptions (WebSocket), graphql-ws, SSE transport |
Architecture Patterns
DataLoader Pattern (N+1 Prevention)
import DataLoader from 'dataloader';
// Create per-request DataLoader
function createLoaders() {
return {
userById: new DataLoader<string, User>(async (ids) => {
// Single batch query instead of N queries
const users = await db.query('SELECT * FROM users WHERE id = ANY($1)', [ids]);
const map = new Map(users.map(u => [u.id, u]));
return ids.map(id => map.get(id) ?? new Error(`User ${id} not found`));
}),
};
}
// Resolver uses loader — automatically batched
const resolvers = {
Post: {
author: (post, _, { loaders }) => loaders.userById.load(post.authorId),
},
};
Apollo Federation 2 (Supergraph)
Clients
↓
Apollo Router (supergraph)
├─→ Users Subgraph (owns User type)
├─→ Orders Subgraph (extends User with orders)
└─→ Products Subgraph (owns Product type)
Each subgraph is an independent GraphQL service.
Router composes query plans across subgraphs.
# Users subgraph
type User @key(fields: "id") {
id: ID!
name: String!
email: String!
}
# Orders subgraph — extends User from Users subgraph
type User @key(fields: "id") {
id: ID!
orders: [Order!]!
}
type Order {
id: ID!
total: Float!
status: OrderStatus!
}
Query Complexity Limiting
import { createComplexityLimitRule } from 'graphql-validation-complexity';
const complexityLimit = createComplexityLimitRule(1000, {
scalarCost: 1,
objectCost: 10,
listFactor: 20,
onCost: (cost) => {
if (cost > 800) logger.warn(`High complexity query: ${cost}`);
},
});
const server = createYoga({
schema,
validationRules: [complexityLimit],
});
Anti-Patterns
- No DataLoader — resolvers that query the DB individually create N+1 problems; always batch with DataLoader
- Unbounded queries — without depth/complexity limits, a single query can join your entire graph and crash the server
- Exposing internal IDs — use opaque Relay-style global IDs, not raw database primary keys
- Fat resolvers — resolvers should delegate to service/repository layers, not contain business logic
- Subscription over-fetching — do not broadcast full objects; send minimal change payloads and let clients re-query
Quality Checklist
1---2name: graphql-server-architect3description: DataLoader, subscriptions, federation, and schema stitching for GraphQL APIs. Activate on: GraphQL, DataLoader, subscription, federation, schema stitching, resolver, SDL, Apollo, Yoga. NOT for: REST API design (use api-architect), frontend GraphQL clients (use relevant frontend skill).4license: Apache-2.05---6
7# GraphQL Server Architect
8
9Design production-grade GraphQL APIs with efficient data loading, real-time subscriptions, and federated schema architecture.
10
11## Activation Triggers
12
13**Activate on:** "GraphQL", "DataLoader", "subscription", "federation", "schema stitching", "resolver", "SDL", "Apollo Server", "GraphQL Yoga", "Pothos", "query complexity"
14
15**NOT for:** REST API design → `api-architect` | Frontend GraphQL client → relevant frontend skill | Database queries → `data-warehouse-optimizer`
16
17## Quick Start
18
191. **Choose framework** — GraphQL Yoga 5.x (lightweight), Apollo Server 4.x (ecosystem), Mercurius (Fastify)
202. **Schema-first or code-first** — SDL for team collaboration, Pothos/Nexus for type-safe code-first
213. **Implement DataLoader** — batch and cache per-request to solve N+1 queries
224. **Set query complexity limits** — prevent clients from requesting arbitrarily deep/wide queries
235. **Add persisted queries** — lock down production to known queries, improve CDN caching
24
25## Core Capabilities
26
27| Domain | Technologies |
28|--------|-------------|
29| **Servers** | GraphQL Yoga 5.x, Apollo Server 4.x, Mercurius 14+ |
30| **Schema** | Pothos (code-first), SDL (schema-first), GraphQL Codegen |
31| **Federation** | Apollo Federation 2.8+, GraphQL Mesh, Schema Stitching |
32| **Performance** | DataLoader, @defer/@stream, persisted queries, query complexity |
33| **Real-Time** | GraphQL Subscriptions (WebSocket), graphql-ws, SSE transport |
34
35## Architecture Patterns
36
37### DataLoader Pattern (N+1 Prevention)
38
39```typescript
40import DataLoader from 'dataloader';
41
42// Create per-request DataLoader
43function createLoaders() {
44 return {
45 userById: new DataLoader<string, User>(async (ids) => {
46 // Single batch query instead of N queries
47 const users = await db.query('SELECT * FROM users WHERE id = ANY($1)', [ids]);
48 const map = new Map(users.map(u => [u.id, u]));
49 return ids.map(id => map.get(id) ?? new Error(`User ${id} not found`));
50 }),
51 };
52}
53
54// Resolver uses loader — automatically batched
55const resolvers = {
56 Post: {
57 author: (post, _, { loaders }) => loaders.userById.load(post.authorId),
58 },
59};
60```
61
62### Apollo Federation 2 (Supergraph)
63
64```
65Clients
66 ↓
67Apollo Router (supergraph)
68 ├─→ Users Subgraph (owns User type)
69 ├─→ Orders Subgraph (extends User with orders)
70 └─→ Products Subgraph (owns Product type)
71
72Each subgraph is an independent GraphQL service.
73Router composes query plans across subgraphs.
74```
75
76```graphql
77# Users subgraph
78type User @key(fields: "id") {
79 id: ID!
80 name: String!
81 email: String!
82}
83
84# Orders subgraph — extends User from Users subgraph
85type User @key(fields: "id") {
86 id: ID!
87 orders: [Order!]!
88}
89
90type Order {
91 id: ID!
92 total: Float!
93 status: OrderStatus!
94}
95```
96
97### Query Complexity Limiting
98
99```typescript
100import { createComplexityLimitRule } from 'graphql-validation-complexity';
101
102const complexityLimit = createComplexityLimitRule(1000, {
103 scalarCost: 1,
104 objectCost: 10,
105 listFactor: 20,
106 onCost: (cost) => {
107 if (cost > 800) logger.warn(`High complexity query: ${cost}`);
108 },
109});
110
111const server = createYoga({
112 schema,
113 validationRules: [complexityLimit],
114});
115```
116
117## Anti-Patterns
118
1191. **No DataLoader** — resolvers that query the DB individually create N+1 problems; always batch with DataLoader
1202. **Unbounded queries** — without depth/complexity limits, a single query can join your entire graph and crash the server
1213. **Exposing internal IDs** — use opaque Relay-style global IDs, not raw database primary keys
1224. **Fat resolvers** — resolvers should delegate to service/repository layers, not contain business logic
1235. **Subscription over-fetching** — do not broadcast full objects; send minimal change payloads and let clients re-query
124
125## Quality Checklist
126
127- [ ] DataLoader used for all relationship resolvers (no N+1)
128- [ ] Query depth limit set (max 10-15 levels)
129- [ ] Query complexity limit enforced (reject expensive queries)
130- [ ] Persisted queries enabled in production (no arbitrary queries from clients)
131- [ ] Schema published to registry with breaking change detection
132- [ ] Resolver-level tracing enabled (Apollo Studio, OpenTelemetry)
133- [ ] Error masking: internal errors never leak to clients in production
134- [ ] Pagination uses Relay cursor-based spec (not offset)
135- [ ] Input validation on all mutation arguments
136- [ ] Subscription authentication verified on connection init