GraphQL Fundamentals
GraphQL is a query language and runtime for APIs. The client specifies what data it needs; the server returns exactly that. A single endpoint replaces dozens of REST endpoints. The schema is first-class; tooling is excellent. The trade-off: more complex than REST.
This page is about how GraphQL works in practice and when to use it.
Schema
A GraphQL schema defines types and their fields:
```graphql
type Order {
id: ID!
amount: Float!
status: OrderStatus!
customer: Customer!
items: [OrderItem!]!
}
enum OrderStatus {
PENDING
SHIPPED
DELIVERED
CANCELLED
}
type Query {
order(id: ID!): Order
orders(status: OrderStatus, limit: Int = 50): [Order!]!
}
type Mutation {
createOrder(input: CreateOrderInput!): Order!
cancelOrder(id: ID!): Order!
}
```
The schema is the contract. Clients query it; servers implement it.
Queries
Clients request specific fields:
```graphql
query {
order(id: "abc") {
id
amount
customer {
name
}
}
}
```
Response contains only the requested fields. No over-fetching.
Aliases
Same field, different arguments:
```graphql
query {
pending: orders(status: PENDING) { id }
shipped: orders(status: SHIPPED) { id }
}
```
Fragments
Reusable field selections:
```graphql
fragment OrderSummary on Order {
id
amount
status
}
query {
order(id: "1") { ...OrderSummary }
orders { ...OrderSummary }
}
```
Variables
Parameterized queries (the right way; not string interpolation):
```graphql
query GetOrder($id: ID!) {
order(id: $id) { id amount }
}
```
Mutations
Operations that modify data:
```graphql
mutation {
createOrder(input: { customerId: "abc", amount: 100.00 }) {
id
status
}
}
```
Same schema/query mechanics as queries; convention separates them so it's clear what modifies state.
Subscriptions
Real-time data via WebSocket (typically):
```graphql
subscription {
orderStatusChanged(orderId: "abc") {
id
status
}
}
```
The connection stays open; the server pushes events. See [WebSocketPatterns](WebSocketPatterns).
The N+1 problem
GraphQL's flexibility creates a server-side performance issue. Consider:
```graphql
query {
orders {
id
customer { name }
}
}
```
Naive resolution:
1. Fetch all orders (1 query)
2. For each order, fetch its customer (N queries)
Total: N+1 queries.
DataLoader
The standard solution. DataLoader batches requests within a single tick of the event loop:
```javascript
const customerLoader = new DataLoader(async (ids) => {
const customers = await db.customers.findByIds(ids);
return ids.map(id => customers.find(c => c.id === id));
});
// In resolver:
customer: (order) => customerLoader.load(order.customerId)
```
Each `load()` call queues the ID; at the end of the tick, all queued IDs are fetched in one query. N+1 becomes 2.
DataLoader is essential for any non-trivial GraphQL server.
Authorization
Field-level authorization is a real complication. Each field can have different permissions; the resolver enforces them.
Two patterns:
- **Per-field directives**: `@auth(role: ADMIN)` annotations on schema fields
- **Resolver-level checks**: each resolver verifies permissions explicitly
For complex authorization, schema-level approaches help; for simple cases, resolver checks are clearest.
Pagination
Three styles, in order of decreasing convenience and increasing scalability:
Offset/limit
```graphql
orders(limit: 50, offset: 100): [Order!]!
```
Simple but inefficient at deep pages.
Cursor-based
```graphql
orders(first: 50, after: "cursor123"): OrderConnection!
type OrderConnection {
edges: [OrderEdge!]!
pageInfo: PageInfo!
}
```
Relay-style. Faster for deep pagination; preserves order under inserts.
Page-based
```graphql
orders(page: 5, perPage: 50): [Order!]!
```
Familiar; less efficient than cursor-based.
See [PaginationStrategies](PaginationStrategies).
When GraphQL wins
- Mobile and web clients with different data needs
- Complex nested data fetching
- Multiple frontends sharing a backend
- Strong typing and schema-driven workflows
When REST wins
- Simple CRUD apps
- Public APIs with diverse client capabilities
- HTTP caching is critical
- Team unfamiliar with GraphQL
See [ApiProtocolComparison](ApiProtocolComparison).
Common failure patterns
- **No DataLoader.** N+1 queries kill performance.
- **Field-level authorization in many places.** Centralize via directives or middleware.
- **Returning all fields by default.** GraphQL's value is selective fetching; resolvers should respect that.
- **Mutating in queries.** Mutations should be in `Mutation`, not as side-effecting `Query` resolvers.
- **Schema evolution as breaking changes.** Add fields freely; deprecate before removing; never remove without warning.
- **Client-side caching neglect.** Apollo Client and similar provide normalized caching; use it.
Further Reading
- [ApiProtocolComparison](ApiProtocolComparison) — REST vs. GraphQL
- [HateoasAndHypermediaApis](HateoasAndHypermediaApis) — REST hypermedia approach
- [PaginationStrategies](PaginationStrategies) — Pagination in GraphQL
- [IdempotencyPatterns](IdempotencyPatterns) — Idempotency for mutations
- [WebServicesAndApis Hub](WebServicesAndApisHub) — Cluster index