Loading...
Loading...
Compare original and translation side by side
| Strategy | Best For | Complexity |
|---|---|---|
| Wrapper | Quick start, no backend changes | Low |
| Gradual | Large APIs, production systems | Medium |
| Rewrite | Greenfield opportunity | High |
| 策略 | 适用场景 | 复杂度 |
|---|---|---|
| 包装器 | 快速启动,无需修改后端 | 低 |
| 渐进式 | 大型API、生产系统 | 中 |
| 重写 | 全新项目机会 | 高 |
// 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`);
}
}// 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 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,
};
}// 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);
}
}// 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);
}
}undefinedundefinedundefinedundefined// 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);
},
},
};// 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);
},
},
};// 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);
},
}// 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);
},
}// 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(),
}),
});// 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(),
}),
});// 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);
},
},
};// 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);
},
},
};// 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);
},
},
};// 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);
}
}// 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);
}
}// 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
},
});// 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!
}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!
}