Agent skill
rest-to-graphql-migrator
Migrates REST APIs to GraphQL incrementally with schema stitching, REST datasources, and gradual endpoint migration. Use when users request "migrate to GraphQL", "REST to GraphQL", "GraphQL wrapper", or "API modernization".
Install this agent skill to your Project
npx add-skill https://github.com/patricio0312rev/skills/tree/main/backend/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
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
rate-limiting-abuse-protection
Implements rate limiting and abuse prevention with per-route policies, IP/user-based limits, sliding windows, safe error responses, and observability. Use when adding "rate limiting", "API protection", "abuse prevention", or "DDoS protection".
rbac-permissions-builder
Implements role-based access control with permission matrix, route guards, policy functions, and UI permission hints. Provides middleware/guards, helper utilities, test suggestions, and permission checking patterns. Use when building "RBAC", "permissions", "access control", or "authorization".
websocket-realtime-builder
Implements real-time features using WebSockets with Socket.io, rooms, authentication, and reconnection handling. Use when users request "real-time updates", "WebSocket", "Socket.io", "live chat", or "push notifications".
webhook-receiver-hardener
Secures webhook receivers with signature verification, retry handling, deduplication, idempotency keys, and error responses. Provides verification code, dedupe storage strategy, runbook for incidents. Use when implementing "webhooks", "webhook security", "event receivers", or "third-party integrations".
auth-module-builder
Implements secure authentication patterns including login/registration, session management, JWT tokens, password hashing, cookie settings, and CSRF protection. Provides auth routes, middleware, security configurations, and threat model documentation. Use when building "authentication", "login system", "JWT auth", or "session management".
queue-job-processor
Implements background job processing with BullMQ/Redis including job queues, workers, scheduling, retries, and monitoring. Use when users request "background jobs", "queue processing", "async tasks", "BullMQ", or "job scheduler".
Didn't find tool you were looking for?