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