mcpField
R
rest-to-graphql-migrator

rest-to-graphql-migrator

github.com/patricio0312rev开发63265 次安装41 周前更新
代码仓库

关于 rest-to-graphql-migrator

面向 API 团队的 REST 到 GraphQL 迁移技能,主张先用包装层连接现有 REST,再逐步迁移端点,同时保留旧客户端兼容。

主要功能

  1. 01分析 REST 端点并设计 GraphQL schema
  2. 02通过 REST 数据源包装现有接口
  3. 03实现 resolver 与数据形状转换
  4. 04使用 DataLoader 减少 N+1 请求
  5. 05规划渐进迁移与 REST 弃用

适用场景

  1. 01在不立即改动后端时接入 GraphQL
  2. 02逐个端点迁移大型生产 API
  3. 03合并多个 REST 服务的 schema
  4. 04逐步引导客户端从 REST 转向 GraphQL

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/patricio0312rev/skills --skill rest-to-graphql-migrator

安装后在 Claude Code 中输入/rest-to-graphql-migrator确认已加载。

详细介绍

REST 到 GraphQL 迁移工具

逐步将 REST API 迁移到 GraphQL,同时不影响现有客户端。

核心工作流

  1. 分析 REST 端点:记录现有 API
  2. 设计 GraphQL 架构:将 REST 映射为类型
  3. 创建 REST 数据源:封装现有端点
  4. 实现解析器:连接到 REST
  5. 逐步迁移:每次迁移一个端点
  6. 弃用 REST:逐步停用

迁移策略

策略对比
策略适用场景复杂度
封装层快速启动,无需更改后端低
渐进式大型 API、生产系统中
重写全新项目机会高

REST 数据源封装层

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`);
  }
}
将 REST 映射为 GraphQL 类型
// 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,
  };
}
带缓存的数据源
// 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 架构设计

根据 REST 端点设计架构
# 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
}

使用 REST 后端的解析器

// 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 防止 N+1 问题

// 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);
  },
}

渐进式迁移

阶段 1:封装层
// 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(),
  }),
});
阶段 2:直接访问数据库
// 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);
    },
  },
};
阶段 3:全面迁移
// 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);
    },
  },
};

错误处理

// 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);
  }
}

架构拼接

合并多个 REST 服务
// 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
  },
});

弃用策略

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!
}

最佳实践

  1. 从封装层开始:不要重写,先封装
  2. 逐步迁移:每次迁移一个端点
  3. 使用 DataLoader:防止 N+1 查询
  4. 转换数据结构:改进 API 设计
  5. 添加缓存:减少 REST API 调用
  6. 妥善处理错误:将 REST 错误映射为 GraphQL 错误
  7. 逐步弃用:给客户端留出迁移时间
  8. 同时监控两者:跟踪 REST 和 GraphQL 的使用情况

输出检查清单

每次 REST 到 GraphQL 迁移都应包括:

  • 已记录 REST 端点
  • 已设计 GraphQL 架构
  • RESTDataSource 实现
  • 数据转换器(REST ↔ GraphQL)
  • 用于批处理的 DataLoader
  • 错误处理和映射
  • 缓存策略
  • 渐进式迁移计划
  • 弃用注解
  • 客户端迁移指南

评论

支持 Markdown