rest-to-graphql-migrator
SKILL.md
REST to GraphQL Migrator
Incrementally migrate REST APIs to GraphQL without breaking existing clients.
Core Workflow
- Analyze REST endpoints: Document existing API
- Design GraphQL schema: Map REST to types
- Create REST data source: Wrap existing endpoints
- Implement resolvers: Connect to REST
- Migrate incrementally: One endpoint at a time
- Deprecate REST: Gradual sunset
Migration Strategies
Strategy Comparison
| Strategy | Best For | Complexity |
|---|---|---|
| Wrapper | Quick start, no backend changes | Low |
| Gradual | Large APIs, production systems | Medium |
| Rewrite | Greenfield opportunity | High |
REST Data Source Wrapper
Apollo RESTDataSource
// datasources/users.datasource.ts
import { RESTDataSource } from '@apollo/datasource-rest';
export class UsersAPI extends RESTDataSource {
override baseURL = process.env.REST_API_URL;
// Add auth header
override willSendRequest(_path: string, request: AugmentedRequest) {
request.headers['Authorization'] = this.context.token;
}
// GET /api/users
async getUsers(params?: { page?: number; limit?: number }) {
const query = new URLSearchParams();
if (params?.page) query.set('page', String(params.page));
if (params?.limit) query.set('limit', String(params.limit));
return this.get<User[]>(`/api/users?${query}`);
}
// GET /api/users/:id
async getUser(id: string) {
return this.get<User>(`/api/users/${id}`);
}
// POST /api/users
async createUser(input: CreateUserInput) {
return this.post<User>('/api/users', { body: input });
}
// PATCH /api/users/:id
async updateUser(id: string, input: UpdateUserInput) {
return this.patch<User>(`/api/users/${id}`, { body: input });
}
// DELETE /api/users/:id
async deleteUser(id: string) {
await this.delete(`/api/users/${id}`);
return true;
}
// GET /api/users/:id/posts
async getUserPosts(userId: string) {
return this.get<Post[]>(`/api/users/${userId}/posts`);
}
}
Map REST to GraphQL Types
// REST Response
interface RESTUser {
id: number;
user_name: string;
email_address: string;
created_at: string;
profile_image_url: string | null;
}
// GraphQL Type (camelCase, proper types)
interface User {
id: string;
username: string;
email: string;
createdAt: Date;
avatar: string | null;
}
// Transformer
function transformUser(restUser: RESTUser): User {
return {
id: String(restUser.id),
username: restUser.user_name,
email: restUser.email_address,
createdAt: new Date(restUser.created_at),
avatar: restUser.profile_image_url,
};
}
Data Source with Caching
// datasources/products.datasource.ts
export class ProductsAPI extends RESTDataSource {
override baseURL = process.env.REST_API_URL;
// Cache for 1 hour
private cacheOptions = { ttl: 3600 };
async getProduct(id: string) {
const data = await this.get<RESTProduct>(`/api/products/${id}`, {
cacheOptions: this.cacheOptions,
});
return transformProduct(data);
}
async getProducts(filters: ProductFilters) {
const params = new URLSearchParams();
Object.entries(filters).forEach(([key, value]) => {
if (value !== undefined) params.set(key, String(value));
});
const data = await this.get<RESTProduct[]>(`/api/products?${params}`);
return data.map(transformProduct);
}
// Bust cache on mutation
async updateProduct(id: string, input: UpdateProductInput) {
const data = await this.patch<RESTProduct>(`/api/products/${id}`, {
body: transformToREST(input),
});
// Invalidate cache
this.delete(`/api/products/${id}`);
return transformProduct(data);
}
}
GraphQL Schema Design
Schema from REST Endpoints
# Map REST endpoints to GraphQL
# REST: GET /api/users
# REST: GET /api/users/:id
# REST: POST /api/users
# REST: PATCH /api/users/:id
# REST: DELETE /api/users/:id
type User {
id: ID!
username: String!
email: String!
avatar: String
createdAt: DateTime!
# Nested resource: GET /api/users/:id/posts
posts: [Post!]!
# Nested resource: GET /api/users/:id/comments
comments: [Comment!]!
}
type Query {
# GET /api/users
users(page: Int, limit: Int): [User!]!
# GET /api/users/:id
user(id: ID!): User
}
type Mutation {
# POST /api/users
createUser(input: CreateUserInput!): User!
# PATCH /api/users/:id
updateUser(id: ID!, input: UpdateUserInput!): User!
# DELETE /api/users/:id
deleteUser(id: ID!): Boolean!
}
input CreateUserInput {
username: String!
email: String!
password: String!
}
input UpdateUserInput {
username: String
email: String
avatar: String
}
Resolvers with REST Backend
// resolvers/user.resolvers.ts
import { Resolvers } from '../generated/types';
export const userResolvers: Resolvers = {
Query: {
users: async (_, { page, limit }, { dataSources }) => {
const users = await dataSources.usersAPI.getUsers({ page, limit });
return users.map(transformUser);
},
user: async (_, { id }, { dataSources }) => {
try {
const user = await dataSources.usersAPI.getUser(id);
return transformUser(user);
} catch (error) {
if (error.extensions?.response?.status === 404) {
return null;
}
throw error;
}
},
},
Mutation: {
createUser: async (_, { input }, { dataSources }) => {
const user = await dataSources.usersAPI.createUser(
transformInputToREST(input)
);
return transformUser(user);
},
updateUser: async (_, { id, input }, { dataSources }) => {
const user = await dataSources.usersAPI.updateUser(
id,
transformInputToREST(input)
);
return transformUser(user);
},
deleteUser: async (_, { id }, { dataSources }) => {
return dataSources.usersAPI.deleteUser(id);
},
},
User: {
// Resolve nested resources
posts: async (parent, _, { dataSources }) => {
const posts = await dataSources.usersAPI.getUserPosts(parent.id);
return posts.map(transformPost);
},
comments: async (parent, _, { dataSources }) => {
const comments = await dataSources.usersAPI.getUserComments(parent.id);
return comments.map(transformComment);
},
},
};
DataLoader for N+1 Prevention
// loaders/user.loader.ts
import DataLoader from 'dataloader';
import { UsersAPI } from '../datasources/users.datasource';
export function createUserLoader(usersAPI: UsersAPI) {
return new DataLoader<string, User>(async (ids) => {
// Batch REST calls or use batch endpoint if available
// Option 1: Parallel individual calls
const users = await Promise.all(
ids.map((id) => usersAPI.getUser(id).catch(() => null))
);
return ids.map((id) => users.find((u) => u?.id === id) || null);
// Option 2: Use batch endpoint if available
// const users = await usersAPI.getUsersByIds([...ids]);
// return ids.map(id => users.find(u => u.id === id) || null);
});
}
// Usage in resolver
User: {
author: async (parent, _, { loaders }) => {
return loaders.users.load(parent.authorId);
},
}
Incremental Migration
Phase 1: Wrapper Layer
// Start with GraphQL wrapping REST
// All data still flows through REST API
const server = new ApolloServer({
typeDefs,
resolvers,
dataSources: () => ({
usersAPI: new UsersAPI(),
postsAPI: new PostsAPI(),
commentsAPI: new CommentsAPI(),
}),
});
Phase 2: Direct Database Access
// Migrate critical paths to direct DB
// Keep REST as fallback
const userResolvers: Resolvers = {
Query: {
user: async (_, { id }, { dataSources, db }) => {
// Try direct DB first
const user = await db.user.findUnique({ where: { id } });
if (user) return user;
// Fallback to REST
return dataSources.usersAPI.getUser(id);
},
},
};
Phase 3: Full Migration
// All data from database
// REST endpoints deprecated
const userResolvers: Resolvers = {
Query: {
user: async (_, { id }, { db }) => {
return db.user.findUnique({
where: { id },
});
},
users: async (_, { page = 1, limit = 20 }, { db }) => {
return db.user.findMany({
skip: (page - 1) * limit,
take: limit,
});
},
},
User: {
posts: async (parent, _, { loaders }) => {
return loaders.postsByAuthor.load(parent.id);
},
},
};
Error Handling
// Map REST errors to GraphQL errors
import { GraphQLError } from 'graphql';
function handleRESTError(error: any): never {
const status = error.extensions?.response?.status;
const body = error.extensions?.response?.body;
switch (status) {
case 400:
throw new GraphQLError(body?.message || 'Bad request', {
extensions: { code: 'BAD_USER_INPUT' },
});
case 401:
throw new GraphQLError('Not authenticated', {
extensions: { code: 'UNAUTHENTICATED' },
});
case 403:
throw new GraphQLError('Not authorized', {
extensions: { code: 'FORBIDDEN' },
});
case 404:
throw new GraphQLError('Resource not found', {
extensions: { code: 'NOT_FOUND' },
});
case 409:
throw new GraphQLError(body?.message || 'Conflict', {
extensions: { code: 'CONFLICT' },
});
default:
throw new GraphQLError('Internal server error', {
extensions: { code: 'INTERNAL_SERVER_ERROR' },
});
}
}
// Usage in resolver
async getUser(id: string) {
try {
return await this.dataSources.usersAPI.getUser(id);
} catch (error) {
handleRESTError(error);
}
}
Schema Stitching
Combine Multiple REST Services
// Stitch multiple REST backends
import { stitchSchemas } from '@graphql-tools/stitch';
const usersSchema = makeExecutableSchema({
typeDefs: usersTypeDefs,
resolvers: usersResolvers,
});
const productsSchema = makeExecutableSchema({
typeDefs: productsTypeDefs,
resolvers: productsResolvers,
});
const ordersSchema = makeExecutableSchema({
typeDefs: ordersTypeDefs,
resolvers: ordersResolvers,
});
const gatewaySchema = stitchSchemas({
subschemas: [
{ schema: usersSchema },
{ schema: productsSchema },
{ schema: ordersSchema },
],
typeMergingOptions: {
// Configure type merging
},
});
Deprecation Strategy
type Query {
# Old endpoint - deprecated
getUser(id: ID!): User @deprecated(reason: "Use user(id:) instead")
# New endpoint
user(id: ID!): User
}
type User {
# Deprecated field with migration path
userName: String @deprecated(reason: "Use username instead")
username: String!
}
Best Practices
- Start with wrapper: Don't rewrite, wrap
- Migrate incrementally: One endpoint at a time
- Use DataLoader: Prevent N+1 queries
- Transform data shapes: Improve API design
- Add caching: Reduce REST API calls
- Handle errors properly: Map REST errors to GraphQL
- Deprecate gradually: Give clients time to migrate
- Monitor both: Track REST and GraphQL usage
Output Checklist
Every REST to GraphQL migration should include:
- REST endpoints documented
- GraphQL schema designed
- RESTDataSource implementations
- Data transformers (REST ↔ GraphQL)
- DataLoader for batching
- Error handling and mapping
- Caching strategy
- Incremental migration plan
- Deprecation annotations
- Client migration guide
Weekly Installs
10
Repository
patricio0312rev/skillsFirst Seen
10 days ago
Installed on
claude-code8
gemini-cli7
antigravity7
windsurf7
github-copilot7
codex7