---
# === IDENTITY ===
id: software/migrations/rest-to-graphql/2026
canonical_question: "How do I migrate a REST API to GraphQL?"
aliases:
  - "REST to GraphQL migration guide"
  - "convert REST API to GraphQL"
  - "replace REST endpoints with GraphQL"
  - "wrap REST API in GraphQL layer"
  - "incremental REST to GraphQL migration"
  - "GraphQL migration from REST best practices"
  - "REST API to GraphQL schema design"
  - "Apollo Connectors REST to GraphQL"
entity_type: software_reference
domain: software > migrations > rest_to_graphql
region: global
jurisdiction: global
temporal_scope: 2020-2026

# === VERIFICATION ===
last_verified: 2026-05-17
confidence: 0.93
version: 2.1
first_published: 2026-02-17

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: evolving
  last_breaking_change: "Strawberry 0.285 (2026-Q1) — Federation v1 removed; Apollo Server 4 EOL 2026-01-26; Strawberry dropped Python 3.9"
  next_review: 2026-11-13
  change_sensitivity: medium

# === CONSTRAINTS ===
constraints:
  - "Apollo Server 4 reached EOL on 2026-01-26 — use Apollo Server 5+ for new projects (requires Node.js 20+, graphql >= 16.11.0)"
  - "DataLoader instances MUST be created per-request in the context factory — a global instance caches data across requests, serving stale data"
  - "Always set maxDepth and maxComplexity limits on production GraphQL endpoints — unbounded query depth enables denial-of-service"
  - "GraphQL responses always return HTTP 200 by default — monitoring tools that rely on HTTP status codes need reconfiguration (Apollo Server 5 restores HTTP 400 for variable coercion errors only)"
  - "Strawberry GraphQL requires Python 3.10+ since v0.284.0 (early 2026) — Python 3.9 support was dropped"
  - "Strawberry Federation v1 was removed in v0.285.0 — all federated schemas must use Federation v2 (replace enable_federation_2 with federation_version)"
  - "Never auto-generate GraphQL schema from database tables — this exposes internal column names, FKs, and audit fields to clients"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "User wants to add GraphQL to a microservices architecture (not migrating from REST)"
    use_instead: "software/patterns/graphql-federation/2026"
  - condition: "User needs REST API design patterns (not migrating away from REST)"
    use_instead: "software/patterns/rest-api-design/2026"
  - condition: "User is comparing GraphQL vs REST for a new project (no existing REST API)"
    use_instead: "software/patterns/api-architecture-comparison/2026"

# === AGENT HINTS ===
inputs_needed:
  - key: backend_language
    question: "What language/framework is your REST API built with?"
    type: choice
    options: ["Node.js/Express", "Python/Django/Flask", "Java/Spring", "Go", ".NET", "Other"]
  - key: client_count
    question: "How many different clients consume the REST API?"
    type: choice
    options: ["One (single SPA/mobile app)", "Multiple (web + mobile + partners)", "Microservices only (backend-to-backend)"]
  - key: migration_approach
    question: "Do you want a code-based or declarative migration approach?"
    type: choice
    options: ["Code-based (Apollo Server + RESTDataSource)", "Declarative (Apollo Connectors — no resolver code)", "Python code-first (Strawberry)", "Python schema-first (Ariadne)"]

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/software/migrations/rest-to-graphql/2026"
suggested_citation: "Source: knowledgelib.io — AI Knowledge Library (verified 2026-05-17)"

# === RELATED UNITS ===
related_kos:
  depends_on:
    - id: "software/patterns/api-versioning/2026"
      label: "API Versioning Patterns"
  related_to:
    - id: "software/migrations/monolith-to-microservices/2026"
      label: "Monolith to Microservices Migration"
    - id: "software/migrations/javascript-to-typescript/2026"
      label: "JavaScript to TypeScript Migration"
    - id: "software/patterns/graphql-schema-patterns/2026"
      label: "GraphQL Schema Design Patterns"
  solves:
    - id: "software/debugging/graphql-n-plus-1-dataloader/2026"
      label: "GraphQL N+1 Query Problem (DataLoader Fix)"
  alternative_to:
    - id: "software/patterns/grpc-service-design/2026"
      label: "gRPC Service Design"
  often_confused_with:
    - id: "software/patterns/graphql-schema-patterns/2026"
      label: "GraphQL Schema Design (designing a schema, not migrating from REST)"

# === SOURCES (10 authoritative sources) ===
# Types: official_docs, technical_blog, rfc_spec, academic_paper, community_resource, industry_report
# Reliability: high, moderate_high, moderate, moderate_low, low, authoritative
sources:
  - id: src1
    title: "Migrating from REST to GraphQL"
    author: GitHub
    url: https://docs.github.com/en/graphql/guides/migrating-from-rest-to-graphql
    type: official_docs
    published: 2024-01-01
    reliability: authoritative
  - id: src2
    title: "Layering GraphQL on top of REST"
    author: Apollo GraphQL
    url: https://www.apollographql.com/blog/backend/layering-graphql-on-top-of-rest/
    type: technical_blog
    published: 2023-06-15
    reliability: high
  - id: src3
    title: "Handling the N+1 Problem"
    author: Apollo GraphQL
    url: https://www.apollographql.com/docs/graphos/schema-design/guides/handling-n-plus-one
    type: official_docs
    published: 2024-06-01
    reliability: authoritative
  - id: src4
    title: "Migrating from REST to GraphQL"
    author: This Dot Labs
    url: https://www.thisdot.co/blog/migrating-from-rest-to-graphql
    type: technical_blog
    published: 2023-09-20
    reliability: high
  - id: src5
    title: "Demand Oriented Schema Design"
    author: Apollo GraphQL
    url: https://www.apollographql.com/docs/technotes/TN0027-demand-oriented-schema-design
    type: official_docs
    published: 2024-03-01
    reliability: authoritative
  - id: src6
    title: "Solving the N+1 Problem for GraphQL through Batching"
    author: Shopify Engineering
    url: https://shopify.engineering/solving-the-n-1-problem-for-graphql-through-batching
    type: technical_blog
    published: 2023-04-10
    reliability: high
  - id: src7
    title: "Using GraphQL with Python — A Complete Guide"
    author: Apollo GraphQL
    url: https://www.apollographql.com/blog/graphql/python/complete-api-guide/
    type: technical_blog
    published: 2023-08-15
    reliability: high
  - id: src8
    title: "Migrating from Apollo Server 4 to Apollo Server 5"
    author: Apollo GraphQL
    url: https://www.apollographql.com/docs/apollo-server/migration
    type: official_docs
    published: 2025-10-01
    reliability: authoritative
  - id: src9
    title: "Apollo Connectors for REST APIs — General Availability"
    author: Apollo GraphQL
    url: https://www.apollographql.com/docs/graphos/connectors
    type: official_docs
    published: 2025-02-19
    reliability: authoritative
  - id: src10
    title: "Strawberry GraphQL Changelog (v0.315.4, May 2026)"
    author: Strawberry GraphQL
    url: https://strawberry.rocks/changelog
    type: official_docs
    published: 2026-05-12
    reliability: authoritative
---

# How to Migrate a REST API to GraphQL

## TL;DR

- **Bottom line**: Migrate incrementally by layering a GraphQL server on top of existing REST endpoints using resolvers that call your REST API as a data source, then gradually move resolver logic to direct database/service calls once the schema stabilizes. For 2025+, Apollo Connectors offer a declarative zero-code alternative for wrapping REST endpoints.
- **Key tool/command**: `@apollo/datasource-rest` (Node.js code-based) or Apollo Connectors `@connect` directive (declarative, no resolver code) or `strawberry` / `ariadne` (Python) to wrap REST endpoints in GraphQL resolvers without rewriting backends.
- **Watch out for**: The N+1 problem — naive resolvers call your REST API once per list item. Use DataLoader to batch and deduplicate requests.
- **Works with (May 2026)**: Apollo Server 5.x (Node.js 20+), Strawberry 0.315+ (Python 3.10+, Federation v2 only), Ariadne 0.23+ (Python 3.10–3.14), GraphQL.js 16.11+, Apollo Connectors (GA via GraphOS Router 2.0). GraphQL Yoga is a lighter performance-focused alternative on Node.js.

## Constraints
<!-- Agents: read this section before recommending any action from this unit.
     These are hard constraints that must not be violated. -->

- Apollo Server 4 reached EOL on 2026-01-26 — use Apollo Server 5+ for new projects (requires Node.js 20+, `graphql` >= 16.11.0). [src8]
- DataLoader instances MUST be created per-request in the context factory — a global instance caches data across requests, serving stale data to different users. [src3]
- Always set `maxDepth` and `maxComplexity` limits on production GraphQL endpoints — unbounded query depth enables denial-of-service attacks. [src4]
- GraphQL responses default to HTTP 200 — monitoring tools that rely on HTTP status codes need reconfiguration to inspect the `errors` array. Apollo Server 5 restores HTTP 400 for variable-coercion errors only. [src1, src8]
- Strawberry GraphQL requires Python 3.10+ since v0.284.0 (early 2026) — Python 3.9 support was dropped. Latest stable as of May 2026: v0.315.4. [src7, src10]
- Strawberry Federation v1 was removed in v0.285.0 (Q1 2026) — federated schemas must use Federation v2 (`federation_version` parameter replaces `enable_federation_2`). [src10]
- Never auto-generate GraphQL schema from database tables — this exposes internal column names, foreign keys, and audit fields to API consumers. Design schemas around client data needs (demand-oriented design). [src5]

## Quick Reference

| REST Concept | GraphQL Equivalent | Example |
|---|---|---|
| `GET /users` | `query { users { id name } }` | Single query, client picks fields |
| `GET /users/123` | `query { user(id: "123") { name email } }` | Arguments replace path params |
| `POST /users` with JSON body | `mutation { createUser(input: { name: "Alice" }) { id } }` | Mutations for writes |
| `PUT /users/123` | `mutation { updateUser(id: "123", input: { name: "Bob" }) { id } }` | Partial updates via input types |
| `DELETE /users/123` | `mutation { deleteUser(id: "123") { success } }` | Return confirmation payload |
| Multiple endpoints for related data | Nested fields: `user { posts { title } }` | One query replaces N requests |
| Query string filters `?status=active` | Field arguments: `users(status: ACTIVE)` | Enum types for filter values |
| Pagination via `?page=2&limit=10` | Cursor-based: `users(first: 10, after: "cursor")` | Relay-style connection pattern |
| HTTP status codes (404, 500) | `errors` array in response body | Errors coexist with partial data |
| API versioning `/v1/`, `/v2/` | Schema evolution — deprecate fields | `@deprecated(reason: "Use newField")` |
| Content negotiation (Accept header) | Field selection in query | Client requests exactly what it needs |
| HATEOAS links | Type relationships in schema | `User.posts` resolves related data |
| Rate limiting per endpoint | Query complexity analysis | Limit total cost per query |
| OpenAPI/Swagger spec | GraphQL SDL schema | Introspection replaces API docs |
| Webhook callbacks | Subscriptions | `subscription { orderUpdated { id status } }` |

## Decision Tree

```
START
├── Is the REST API stable with well-documented endpoints?
│   ├── YES → Layer GraphQL on top using RESTDataSource resolvers (see Step 2)
│   └── NO → Stabilize REST API first, then migrate
├── How many clients consume the API?
│   ├── ONE (e.g., single SPA) → Migrate client + server together, feature by feature
│   └── MULTIPLE → Keep REST running, add GraphQL as new endpoint, migrate clients one at a time
├── Is over-fetching / under-fetching a real problem?
│   ├── YES → GraphQL will provide immediate value — prioritize high-traffic endpoints first
│   └── NO → Consider if migration complexity is worth it (see "When Not to Use" below)
├── Do you want a code-based or declarative approach?
│   ├── DECLARATIVE → Use Apollo Connectors @connect directive (no resolver code, GA since Feb 2025) [src9]
│   └── CODE-BASED ↓
├── Are you using microservices?
│   ├── YES → Use Apollo Federation v2 or schema stitching to compose a unified graph
│   └── NO (monolith) → Single GraphQL server with resolvers calling internal services
├── Is your backend Node.js or Python?
│   ├── NODE.JS → Apollo Server 5 + @apollo/datasource-rest [src8]
│   ├── PYTHON → Strawberry 0.303+ (code-first) or Ariadne 0.23+ (schema-first)
│   └── OTHER → graphql-java, gqlgen (Go), Hot Chocolate (.NET)
└── DEFAULT → Start with one feature, wrap REST endpoints in resolvers, expand incrementally
```

## Step-by-Step Guide

### 1. Audit REST endpoints and map to a GraphQL schema

Inventory all REST endpoints, their request/response shapes, and relationships between resources. Design a demand-oriented GraphQL schema based on how clients consume data, not how your backend stores it. [src1, src5]

```bash
# List all REST endpoints from an OpenAPI spec
cat openapi.json | jq '.paths | keys[]'

# Count endpoints by HTTP method
cat openapi.json | jq '[.paths[][]] | group_by(.operationId) | length'
```

**Verify**: The schema draft covers all client use cases. Compare the GraphQL type list against REST resource nouns — every resource should map to at least one type.

### 2. Set up a GraphQL server that wraps existing REST endpoints

Install a GraphQL server and create resolvers that delegate to your existing REST API. This is the zero-risk starting point — no backend changes needed. [src2, src4]

```javascript
// Node.js: Apollo Server 5 + @apollo/datasource-rest
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { RESTDataSource } from '@apollo/datasource-rest';

class UsersAPI extends RESTDataSource {
  baseURL = 'https://api.example.com/v1/';

  async getUsers() {
    return this.get('users');
  }

  async getUser(id) {
    return this.get(`users/${encodeURIComponent(id)}`);
  }

  async getUserPosts(userId) {
    return this.get(`users/${encodeURIComponent(userId)}/posts`);
  }
}

const typeDefs = `#graphql
  type User {
    id: ID!
    name: String!
    email: String!
    posts: [Post!]!
  }

  type Post {
    id: ID!
    title: String!
    content: String!
    author: User!
  }

  type Query {
    users: [User!]!
    user(id: ID!): User
  }
`;

const resolvers = {
  Query: {
    users: (_, __, { dataSources }) => dataSources.usersAPI.getUsers(),
    user: (_, { id }, { dataSources }) => dataSources.usersAPI.getUser(id),
  },
  User: {
    posts: (parent, _, { dataSources }) =>
      dataSources.usersAPI.getUserPosts(parent.id),
  },
};

const server = new ApolloServer({ typeDefs, resolvers });

const { url } = await startStandaloneServer(server, {
  context: async () => ({
    dataSources: {
      usersAPI: new UsersAPI(),
    },
  }),
});
console.log(`GraphQL server ready at ${url}`);
```

**Verify**: `curl -X POST http://localhost:4000/ -H 'Content-Type: application/json' -d '{"query":"{ users { id name } }"}'` returns data from your REST API.

### 3. Add DataLoader to prevent N+1 queries

Every resolver that fetches related data (e.g., `User.posts`) will cause N+1 requests to your REST API. Add DataLoader to batch and deduplicate these calls. [src3, src6]

```javascript
import DataLoader from 'dataloader';

// Batch function: receives array of user IDs, returns array of post arrays
async function batchUserPosts(userIds) {
  // Single REST call with all IDs (if your API supports it)
  const results = await fetch(
    `https://api.example.com/v1/posts?userIds=${userIds.join(',')}`
  ).then(r => r.json());

  // Map results back to input order
  return userIds.map(id =>
    results.filter(post => post.userId === id)
  );
}

// Create per-request DataLoader (MUST be per-request, not global)
function createLoaders() {
  return {
    userPosts: new DataLoader(batchUserPosts),
  };
}

// Updated resolver
const resolvers = {
  User: {
    posts: (parent, _, { loaders }) => loaders.userPosts.load(parent.id),
  },
};

// Updated context
const { url } = await startStandaloneServer(server, {
  context: async () => ({
    dataSources: { usersAPI: new UsersAPI() },
    loaders: createLoaders(),
  }),
});
```

**Verify**: Query `{ users { id posts { title } } }` with 10 users. Check server logs — should see 1 request for users + 1 batched request for posts (2 total), not 11.

### 4. Implement mutations for write operations

Map REST POST/PUT/DELETE endpoints to GraphQL mutations. Use input types for structured arguments and return the mutated object for cache updates. [src4, src5]

```graphql
# schema.graphql
input CreateUserInput {
  name: String!
  email: String!
}

input UpdateUserInput {
  name: String
  email: String
}

type Mutation {
  createUser(input: CreateUserInput!): User!
  updateUser(id: ID!, input: UpdateUserInput!): User!
  deleteUser(id: ID!): DeleteResult!
}

type DeleteResult {
  success: Boolean!
  message: String
}
```

```javascript
// Mutation resolvers
const resolvers = {
  Mutation: {
    createUser: async (_, { input }, { dataSources }) => {
      return dataSources.usersAPI.post('users', { body: input });
    },
    updateUser: async (_, { id, input }, { dataSources }) => {
      return dataSources.usersAPI.patch(`users/${encodeURIComponent(id)}`, {
        body: input,
      });
    },
    deleteUser: async (_, { id }, { dataSources }) => {
      await dataSources.usersAPI.delete(`users/${encodeURIComponent(id)}`);
      return { success: true, message: `User ${id} deleted` };
    },
  },
};
```

**Verify**: Execute a `createUser` mutation via GraphQL Playground and confirm the REST API received the POST request with correct body.

### 5. Migrate clients to use GraphQL queries

Update frontend code to use GraphQL queries instead of REST calls. Start with the highest-traffic pages. [src4]

```javascript
// BEFORE: REST client code
const response = await fetch('/api/v1/users/123');
const user = await response.json();
const postsResponse = await fetch(`/api/v1/users/123/posts`);
const posts = await postsResponse.json();

// AFTER: GraphQL client code (using @apollo/client)
import { gql, useQuery } from '@apollo/client';

const USER_WITH_POSTS = gql`
  query UserWithPosts($id: ID!) {
    user(id: $id) {
      id
      name
      email
      posts {
        id
        title
        content
      }
    }
  }
`;

function UserProfile({ userId }) {
  const { data, loading, error } = useQuery(USER_WITH_POSTS, {
    variables: { id: userId },
  });

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;
  return (
    <div>
      <h1>{data.user.name}</h1>
      <ul>
        {data.user.posts.map(post => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </div>
  );
}
```

**Verify**: Network tab shows a single GraphQL POST request replacing multiple REST GET requests. Response payload contains only requested fields.

### 6. Gradually replace REST data sources with direct database/service calls

Once the GraphQL schema is stable and clients are migrated, remove the REST intermediary layer in resolvers. Call your database or internal services directly for better performance. [src2]

```javascript
// BEFORE: Resolver calls REST API
const resolvers = {
  Query: {
    users: (_, __, { dataSources }) => dataSources.usersAPI.getUsers(),
  },
};

// AFTER: Resolver calls database directly
import { pool } from './db.js';

const resolvers = {
  Query: {
    users: async () => {
      const { rows } = await pool.query('SELECT id, name, email FROM users');
      return rows;
    },
  },
};
```

**Verify**: GraphQL responses remain identical. Run integration tests to confirm no regressions.

### 7. Deprecate and retire REST endpoints

Once all clients use GraphQL, deprecate REST endpoints with a sunset timeline. Monitor access logs to confirm zero traffic before removal. [src1, src4]

```bash
# Check for remaining REST API traffic (example with nginx logs)
grep 'GET /api/v1/' access.log | wc -l

# Add deprecation headers to REST responses
# In Express.js middleware:
# res.set('Sunset', 'Sat, 01 Jun 2026 00:00:00 GMT');
# res.set('Deprecation', 'true');
# res.set('Link', '<https://api.example.com/graphql>; rel="successor-version"');
```

**Verify**: REST endpoint access count is zero over a 30-day window before removing the code.

## Code Examples

### Node.js/TypeScript: Complete GraphQL server wrapping a REST API

> Full script: [node-js-typescript-complete-graphql-server-wrappin.ts](scripts/node-js-typescript-complete-graphql-server-wrappin.ts) (86 lines)

```typescript
// Input:  Existing REST API at https://api.example.com/v1/
// Output: GraphQL server that proxies and reshapes REST responses
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { RESTDataSource } from '@apollo/datasource-rest';
# ... (see full script)
```

### Python/Strawberry: GraphQL API wrapping Flask REST endpoints

> Full script: [python-strawberry-graphql-api-wrapping-flask-rest-.py](scripts/python-strawberry-graphql-api-wrapping-flask-rest-.py) (56 lines)

```python
# Input:  Existing Flask REST API
# Output: Strawberry GraphQL server wrapping the REST endpoints
import strawberry
import httpx
from typing import Optional
# ... (see full script)
```

### Python/Ariadne: Schema-first GraphQL wrapping REST

> Full script: [python-ariadne-schema-first-graphql-wrapping-rest.py](scripts/python-ariadne-schema-first-graphql-wrapping-rest.py) (57 lines)

```python
# Input:  Existing REST API
# Output: Ariadne schema-first GraphQL server
from ariadne import QueryType, MutationType, make_executable_schema
from ariadne.asgi import GraphQL
import httpx
# ... (see full script)
```

### Apollo Connectors: Declarative REST-to-GraphQL (no resolver code)

```graphql
# Input:  Existing REST API with OpenAPI spec
# Output: GraphQL schema that maps directly to REST endpoints via @connect directive
# Requires: Apollo GraphOS + Router 2.0 (GA since Feb 2025) [src9]

extend schema
  @link(url: "https://specs.apollo.dev/connect/v0.1", import: ["@connect", "@source"])
  @source(name: "restAPI", http: { baseURL: "https://api.example.com/v1" })

type Query {
  users: [User!]!
    @connect(source: "restAPI", http: { GET: "/users" })

  user(id: ID!): User
    @connect(source: "restAPI", http: { GET: "/users/{$args.id}" })
}

type User {
  id: ID!
  name: String!
  email: String!
  posts: [Post!]!
    @connect(source: "restAPI", http: { GET: "/users/{$this.id}/posts" })
}

type Post {
  id: ID!
  title: String!
  content: String!
}
```

## Anti-Patterns

### Wrong: Mirroring REST endpoint structure in GraphQL schema

```graphql
// ❌ BAD — Schema mimics REST URL hierarchy instead of data relationships
type Query {
  getUsers: [User]           # Named like REST: GET /users
  getUserById(id: ID!): User # Named like REST: GET /users/:id
  postUser(name: String!, email: String!): User  # Named like HTTP verb
  putUser(id: ID!, name: String, email: String): User
  deleteUser(id: ID!): Boolean
}
```

### Correct: Design schema around client data needs

```graphql
// ✅ GOOD — Demand-oriented schema with proper queries/mutations
type Query {
  users(filter: UserFilter): [User!]!
  user(id: ID!): User
}

type Mutation {
  createUser(input: CreateUserInput!): User!
  updateUser(id: ID!, input: UpdateUserInput!): User!
  deleteUser(id: ID!): DeleteResult!
}

input UserFilter {
  status: UserStatus
  role: String
}
```

### Wrong: Creating a new REST call per field resolver (N+1)

```javascript
// ❌ BAD — Each user triggers a separate REST call for posts
const resolvers = {
  User: {
    posts: async (parent) => {
      // Called N times — once per user in the list
      const res = await fetch(`/api/v1/users/${parent.id}/posts`);
      return res.json();
    },
  },
};
```

### Correct: Use DataLoader to batch REST calls

```javascript
// ✅ GOOD — All post lookups batched into a single request
import DataLoader from 'dataloader';

const postLoader = new DataLoader(async (userIds) => {
  const res = await fetch(`/api/v1/posts?userIds=${userIds.join(',')}`);
  const posts = await res.json();
  return userIds.map(id => posts.filter(p => p.userId === id));
});

const resolvers = {
  User: {
    posts: (parent) => postLoader.load(parent.id),
  },
};
```

### Wrong: Auto-generating GraphQL schema from database tables

```javascript
// ❌ BAD — Exposes internal database structure and column names
// Auto-generated types include: user_created_at_utc, fk_department_id,
// is_deleted_flag, internal_audit_notes
// Clients forced to work with backend naming conventions
const typeDefs = generateSchemaFromDB(database);
```

### Correct: Manually design client-facing schema, map in resolvers

```javascript
// ✅ GOOD — Clean client-facing types, resolvers handle mapping
const typeDefs = `#graphql
  type User {
    id: ID!
    name: String!
    department: Department!      # Relationship, not FK
    createdAt: DateTime!         # Clean name, not column name
    # internal_audit_notes NOT exposed
  }
`;

const resolvers = {
  User: {
    department: (user) => getDepartment(user.fk_department_id),
    createdAt: (user) => user.user_created_at_utc,
  },
};
```

### Wrong: Returning HTTP status codes in GraphQL responses

```javascript
// ❌ BAD — Using HTTP semantics inside GraphQL
const resolvers = {
  Query: {
    user: async (_, { id }, { res }) => {
      const user = await findUser(id);
      if (!user) {
        res.status(404);  // GraphQL always returns 200
        return null;
      }
      return user;
    },
  },
};
```

### Correct: Use GraphQL error handling with extensions

```javascript
// ✅ GOOD — GraphQL-native error handling
import { GraphQLError } from 'graphql';

const resolvers = {
  Query: {
    user: async (_, { id }) => {
      const user = await findUser(id);
      if (!user) {
        throw new GraphQLError('User not found', {
          extensions: { code: 'NOT_FOUND', argumentName: 'id' },
        });
      }
      return user;
    },
  },
};
```

### Wrong: Using Apollo Server 4 in new projects (EOL since Jan 2026)

```javascript
// ❌ BAD — Apollo Server 4 is end-of-life as of 2026-01-26
import { ApolloServer } from '@apollo/server'; // v4
import { expressMiddleware } from '@apollo/server/express4'; // v4 built-in Express
```

### Correct: Use Apollo Server 5 with separate integration packages

```javascript
// ✅ GOOD — Apollo Server 5 with explicit framework integration [src8]
import { ApolloServer } from '@apollo/server'; // v5
import { expressMiddleware } from '@as-integrations/express4'; // separate package
// Or for standalone (no longer uses Express internally):
import { startStandaloneServer } from '@apollo/server/standalone';
```

## Common Pitfalls

- **N+1 query problem**: Naive field resolvers call your REST API once per item in a list, creating N+1 total requests. Fix: Use `DataLoader` to batch all IDs into a single request. Create a new DataLoader instance per request to avoid cross-request caching bugs. [src3, src6]
- **Mirroring REST structure in schema**: Copying REST endpoint paths and HTTP verbs into your GraphQL schema defeats the purpose of migration. Fix: Design the schema based on client data consumption patterns (demand-oriented design), not backend structure. [src5]
- **Global DataLoader instances**: DataLoader must be created per-request. A global instance caches data across requests, serving stale data to different users. Fix: Create DataLoader instances in the context factory function, not at module level. [src3]
- **No query complexity limits**: Unlike REST where each endpoint has bounded cost, GraphQL queries can request arbitrarily deep nested data. Fix: Implement query depth limiting and cost analysis using `graphql-query-complexity` or Apollo's built-in limits. [src4]
- **Breaking clients during schema evolution**: Removing fields or changing types without deprecation breaks existing queries. Fix: Use `@deprecated(reason: "Use newField instead")` directive, monitor field usage via Apollo Studio, and only remove fields after zero usage for 90+ days. [src1]
- **Authentication gaps**: REST middleware (API keys, JWT validation) may not automatically apply to GraphQL resolvers. Fix: Implement authentication in the GraphQL context factory, not in individual resolvers. Check auth once per request, not once per field. [src4]
- **Over-fetching from REST in resolvers**: GraphQL resolvers that call REST endpoints still fetch the full REST response even when the client only requests a few fields. Fix: Accept this during migration (the benefit is client-side savings), then later replace REST calls with direct database queries that select only needed columns. [src2]
- **Using EOL Apollo Server 4 in new projects**: Apollo Server 4 reached end-of-life on 2026-01-26. Fix: Use Apollo Server 5 (requires Node.js 20+, `graphql` >= 16.11.0). The upgrade is minimal — mainly updating framework integration imports from built-in to `@as-integrations/*` packages. [src8]

## Diagnostic Commands

```bash
# Test GraphQL endpoint with curl
curl -X POST http://localhost:4000/graphql \
  -H 'Content-Type: application/json' \
  -d '{"query":"{ __schema { types { name } } }"}'

# Introspect schema (get full SDL)
npx graphql-inspector introspect http://localhost:4000/graphql --write schema.graphql

# Check for breaking changes between schema versions
npx graphql-inspector diff old-schema.graphql new-schema.graphql

# Monitor query complexity (Apollo Server plugin)
# Add ApolloServerPluginUsageReporting to server plugins

# Count REST API calls per GraphQL query (add logging to DataSource)
# Override willSendRequest() in RESTDataSource to log each call

# Validate schema against best practices
npx graphql-schema-linter schema.graphql

# Test query performance
npx graphql-benchmark --endpoint http://localhost:4000/graphql --query '{ users { id name posts { title } } }'

# Check Apollo Server version (ensure not on EOL v4)
npm ls @apollo/server
```

## Version History & Compatibility

| Version | Status | Breaking Changes | Migration Notes |
|---|---|---|---|
| Apollo Server 5.x (2025) | Current | Express middleware moved to `@as-integrations/*`; standalone no longer uses Express; Node.js 20+ required | Replace `@apollo/server/express4` with `@as-integrations/express4` [src8] |
| Apollo Server 4.x (2023) | EOL (2026-01-26) | Standalone package, new plugin API | Upgrade to v5 (minimal changes) [src8] |
| Apollo Server 3.x (2021) | EOL | `apollo-datasource-rest` moved to `@apollo/datasource-rest` | Skip to v5 directly |
| Apollo Server 2.x (2018) | EOL | — | Skip to v5 directly |
| Apollo Connectors + GraphOS Router 2.0 (2025) | GA | N/A (new feature) | Declarative `@connect` directive for REST-to-GraphQL without resolvers; native query planner in Router 2.0 [src9] |
| Strawberry 0.315.4 (May 2026) | Current | v0.284 dropped Python 3.9; v0.285 removed Federation v1; v0.312.3 added WebSocket rate-limiting | Require Python 3.10+, replace `enable_federation_2` with `federation_version`, set `max_subscriptions_per_connection` [src10] |
| Strawberry 0.268+ (2025) | EOL | Python 3.9 dropped | Upgrade to 0.315.x |
| Ariadne 0.23+ (2024) | Current | None significant; supports Python 3.10–3.14 | — |
| GraphQL.js 16.x (2022) | Current | Pure ESM, `graphql-tools` v9 | Use ESM imports; required by Apollo Server 5 |
| Apollo Client 4.0 (2025) | Current | Leaner bundles, strengthened TypeScript safety | Update client-side imports |
| GraphQL Yoga 5.x (2025) | Current | Lighter alternative to Apollo Server; SSE subscriptions built-in; no batched queries | Migrate Apollo plugins to envelop plugins; install `@graphql-yoga/plugin-response-cache` for response caching |

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| Multiple clients need different data shapes from the same API | Simple CRUD API with one client | Keep REST |
| Mobile + web clients with different bandwidth needs | File upload is the primary use case | REST with multipart/form-data |
| Frontend fetches from 3+ REST endpoints per page load | Backend-to-backend microservice communication | gRPC or REST |
| You need real-time subscriptions alongside queries | Simple webhooks suffice for async events | REST + webhooks |
| API consumers are internal teams you can coordinate with | Public API with thousands of external consumers | REST with OpenAPI spec |
| Over-fetching is measurably hurting mobile performance | All endpoints return <1 KB responses | REST is fine |
| You want declarative REST orchestration without custom resolvers | You need fine-grained control over every resolver | Apollo Connectors vs code-based resolvers |

## Decision Logic

### If you have an OpenAPI spec and want zero resolver code
--> Use Apollo Connectors `@connect` directive (GA via GraphOS Router 2.0). Declarative mapping from REST endpoints to GraphQL fields with no procedural code. Requires Apollo GraphOS. [src9]

### If your backend is Node.js and you need fine-grained resolver control
--> Apollo Server 5 + `@apollo/datasource-rest` + DataLoader. Mature ecosystem, federation support, plug-in libraries. Requires Node.js 20+. [src2, src8]

### If raw performance matters more than ecosystem (Node.js)
--> GraphQL Yoga 5.x. Lower latency than Apollo Server in benchmarks, lighter footprint, built-in SSE subscriptions. Migrate Apollo plugins to envelop plugins. No batched queries support. [src8]

### If your backend is Python and you prefer type-hint-driven code-first design
--> Strawberry 0.315+ with `httpx.AsyncClient` for REST calls. Requires Python 3.10+; Federation v2 only. [src7, src10]

### If your backend is Python and you have an existing SDL/schema you want to bind resolvers to
--> Ariadne 0.23+ schema-first. Lower learning curve when the schema already exists. Supports Python 3.10–3.14. [src7]

### If you see >5x N+1 amplification on nested fields in load tests
--> Add DataLoader instances per-request in the context factory before going further. Naive resolvers will saturate the upstream REST API. NEVER share a global DataLoader across requests. [src3, src6]

### If you have multiple existing GraphQL services to compose
--> Use Apollo Federation v2 (and Strawberry Federation v2 on Python — v1 was removed in v0.285). Schema stitching is deprecated. [src8, src10]

### If you're starting fresh and worried about future migration
--> Skip the REST passthrough layer entirely. Build resolvers directly against your database/services with DataLoader from day one. [src2]

## Important Caveats

- GraphQL responses always return HTTP 200 — errors are in the response body's `errors` array. Monitoring tools that rely on HTTP status codes need reconfiguration. **Exception**: Apollo Server 5 now returns HTTP 400 for variable coercion errors. [src8]
- Caching is harder with GraphQL because POST requests bypass browser/CDN caches by default. Use persisted queries (Apollo) or GET requests for caching, and implement resolver-level caching with DataLoader or Redis.
- File uploads require special handling — the GraphQL spec does not define file upload. Use the `graphql-upload` package or handle uploads via a separate REST endpoint.
- Query complexity can cause denial-of-service if unbounded. Always set `maxDepth` and `maxComplexity` limits in production.
- Apollo Server 5 no longer bundles Express — the `startStandaloneServer` function uses Node.js built-in HTTP server. For Express integration, install `@as-integrations/express4` (Express v4) or `@as-integrations/express5` (Express v5) separately. [src8]
- DataLoader batching only works within a single event loop tick — if your resolver awaits other async work before calling `loader.load()`, the batch window may close prematurely.
- Apollo Connectors (`@connect` directive) require Apollo GraphOS and Router 2.0 — they are not available in self-hosted open-source Apollo Server alone. [src9]
- Strawberry v0.312.3+ enforces WebSocket connection handshake verification and rate-limits subscriptions via `max_subscriptions_per_connection` (default: 100). If clients exceeded this in older versions, raise the cap explicitly. [src10]
- Strawberry Federation v1 was removed in v0.285.0 — federated Python schemas must migrate to Federation v2 before upgrading. [src10]

## Related Units
<!-- Generated from related_kos frontmatter -->

- [API Versioning Patterns](/software/patterns/api-versioning/2026)
- [Monolith to Microservices Migration](/software/migrations/monolith-to-microservices/2026)
- [JavaScript to TypeScript Migration](/software/migrations/javascript-to-typescript/2026)
- [GraphQL Schema Design Patterns](/software/patterns/graphql-schema-patterns/2026)
- [GraphQL N+1 Query Problem (DataLoader Fix)](/software/debugging/graphql-n-plus-1-dataloader/2026)
- [gRPC Service Design](/software/patterns/grpc-service-design/2026)
