api-design
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseAPI Design - REST & GraphQL
API设计 - REST与GraphQL
Design consistent, intuitive APIs that scale
设计一致、直观且可扩展的API
When to Use This Skill
何时使用此技能
Use this skill when:
- Designing new API endpoints (REST or GraphQL)
- Creating HTTP routes and handlers
- Implementing pagination, filtering, or sorting
- Versioning APIs for backward compatibility
- Handling API errors and validation
- Designing GraphQL schemas and resolvers
- Optimizing API performance (N+1 queries, caching)
Don't use this skill for:
- Frontend-only work with no API involvement
- Direct database queries without an API layer
- Internal function calls (not exposed as API)
在以下场景使用此技能:
- 设计新的API端点(REST或GraphQL)
- 创建HTTP路由与处理器
- 实现分页、过滤或排序功能
- 为向后兼容进行API版本控制
- 处理API错误与验证
- 设计GraphQL Schema与解析器
- 优化API性能(N+1查询、缓存)
请勿在以下场景使用:
- 仅涉及前端且无API交互的工作
- 不通过API层直接进行数据库查询
- 内部函数调用(未暴露为API)
Critical Patterns
核心设计模式
Pattern 1: HTTP Methods and Status Codes
模式1:HTTP方法与状态码
When: Building RESTful endpoints
Good:
typescript
// Correct HTTP methods and status codes
export async function GET(request: Request) {
const users = await db.user.findMany();
return NextResponse.json(users, { status: 200 });
}
export async function POST(request: Request) {
const body = await request.json();
if (!body.email || !body.name) {
return NextResponse.json(
{ error: 'Email and name are required' },
{ status: 400 } // Bad Request
);
}
const user = await db.user.create({ data: body });
return NextResponse.json(user, { status: 201 }); // Created
}
export async function DELETE(request: Request) {
await db.user.delete({ where: { id: '123' } });
return new Response(null, { status: 204 }); // No Content
}Bad:
typescript
// ❌ Wrong: Using POST for everything
export async function POST(request: Request) {
const { action, userId } = await request.json();
if (action === 'get') {
const user = await db.user.findUnique({ where: { id: userId } });
return NextResponse.json(user); // Should be GET
}
if (action === 'delete') {
await db.user.delete({ where: { id: userId } });
return NextResponse.json({ success: true }); // Should be DELETE
}
}
// ❌ Wrong: Always returning 200
export async function GET(request: Request) {
const user = await db.user.findUnique({ where: { id: '999' } });
if (!user) {
return NextResponse.json({ error: 'Not found' }, { status: 200 }); // Should be 404
}
return NextResponse.json(user);
}Why: Correct HTTP methods and status codes make APIs predictable and RESTful. Clients can rely on standard semantics.
HTTP Methods Quick Reference:
GET - Retrieve (Safe, Idempotent, Cacheable)
POST - Create (Not safe, Not idempotent)
PUT - Replace entire resource (Not safe, Idempotent)
PATCH - Partial update (Not safe, Usually idempotent)
DELETE - Remove (Not safe, Idempotent)Status Codes Quick Reference:
2xx Success:
200 OK - Successful GET, PUT, PATCH, DELETE
201 Created - Successful POST (resource created)
204 No Content - Successful DELETE (no response body)
4xx Client Errors:
400 Bad Request - Invalid request data
401 Unauthorized - Authentication required
403 Forbidden - Authenticated but not authorized
404 Not Found - Resource doesn't exist
409 Conflict - Resource conflict (duplicate)
422 Unprocessable - Validation errors
5xx Server Errors:
500 Internal - Unexpected server error
503 Unavailable - Server temporarily unavailable适用场景:构建RESTful端点
正确示例:
typescript
// Correct HTTP methods and status codes
export async function GET(request: Request) {
const users = await db.user.findMany();
return NextResponse.json(users, { status: 200 });
}
export async function POST(request: Request) {
const body = await request.json();
if (!body.email || !body.name) {
return NextResponse.json(
{ error: 'Email and name are required' },
{ status: 400 } // Bad Request
);
}
const user = await db.user.create({ data: body });
return NextResponse.json(user, { status: 201 }); // Created
}
export async function DELETE(request: Request) {
await db.user.delete({ where: { id: '123' } });
return new Response(null, { status: 204 }); // No Content
}错误示例:
typescript
// ❌ Wrong: Using POST for everything
export async function POST(request: Request) {
const { action, userId } = await request.json();
if (action === 'get') {
const user = await db.user.findUnique({ where: { id: userId } });
return NextResponse.json(user); // Should be GET
}
if (action === 'delete') {
await db.user.delete({ where: { id: userId } });
return NextResponse.json({ success: true }); // Should be DELETE
}
}
// ❌ Wrong: Always returning 200
export async function GET(request: Request) {
const user = await db.user.findUnique({ where: { id: '999' } });
if (!user) {
return NextResponse.json({ error: 'Not found' }, { status: 200 }); // Should be 404
}
return NextResponse.json(user);
}原因:正确的HTTP方法与状态码让API具备可预测性且符合REST规范。客户端可以依赖标准语义进行开发。
HTTP方法速查:
GET - 检索资源(安全、幂等、可缓存)
POST - 创建资源(不安全、非幂等)
PUT - 替换整个资源(不安全、幂等)
PATCH - 部分更新资源(不安全、通常幂等)
DELETE - 删除资源(不安全、幂等)状态码速查:
2xx 成功:
200 OK - GET、PUT、PATCH、DELETE请求成功
201 Created - POST请求成功(资源已创建)
204 No Content - DELETE请求成功(无响应体)
4xx 客户端错误:
400 Bad Request - 请求数据无效
401 Unauthorized - 需要身份验证
403 Forbidden - 已认证但无权限
404 Not Found - 资源不存在
409 Conflict - 资源冲突(如重复)
422 Unprocessable - 验证错误
5xx 服务器错误:
500 Internal - 意外服务器错误
503 Unavailable - 服务器暂时不可用Pattern 2: Resource Naming and Nesting
模式2:资源命名与嵌套
When: Designing API URL structure
Good:
typescript
// ✅ Use nouns, not verbs
GET /api/users
POST /api/users
GET /api/users/123
DELETE /api/users/123
// ✅ Plural nouns for collections
GET /api/products
GET /api/orders
// ✅ Nested resources (max 2 levels)
GET /api/users/123/posts
POST /api/users/123/posts
GET /api/users/123/posts/456Bad:
typescript
// ❌ Wrong: Verbs in URLs
GET /api/getUsers
POST /api/createUser
DELETE /api/deleteUser/123
// ❌ Wrong: Singular for collections
GET /api/user
GET /api/product
// ❌ Wrong: Too deeply nested (3+ levels)
GET /api/users/123/posts/456/comments/789/likesWhy: Nouns represent resources, verbs are implied by HTTP methods. Avoid deep nesting to keep URLs simple and predictable.
Nested Resources Pattern:
GET /api/users/123/posts - Get all posts by user 123
GET /api/users/123/posts/456 - Get specific post by user 123
POST /api/users/123/posts - Create post for user 123
// ⚠️ For deep relationships, use query params instead:
GET /api/comments?postId=456
GET /api/likes?commentId=789适用场景:设计API的URL结构
正确示例:
typescript
// ✅ Use nouns, not verbs
GET /api/users
POST /api/users
GET /api/users/123
DELETE /api/users/123
// ✅ Plural nouns for collections
GET /api/products
GET /api/orders
// ✅ Nested resources (max 2 levels)
GET /api/users/123/posts
POST /api/users/123/posts
GET /api/users/123/posts/456错误示例:
typescript
// ❌ Wrong: Verbs in URLs
GET /api/getUsers
POST /api/createUser
DELETE /api/deleteUser/123
// ❌ Wrong: Singular for collections
GET /api/user
GET /api/product
// ❌ Wrong: Too deeply nested (3+ levels)
GET /api/users/123/posts/456/comments/789/likes原因:名词代表资源,动词由HTTP方法隐含。避免过深嵌套以保持URL简洁且可预测。
资源嵌套模式:
GET /api/users/123/posts - 获取用户123的所有文章
GET /api/users/123/posts/456 - 获取用户123的指定文章
POST /api/users/123/posts - 为用户123创建文章
// ⚠️ 对于深层关联,使用查询参数替代:
GET /api/comments?postId=456
GET /api/likes?commentId=789Pattern 3: Pagination
模式3:分页机制
When: Returning large collections
Good - Cursor-based (recommended for large datasets):
typescript
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const cursor = searchParams.get('cursor');
const limit = parseInt(searchParams.get('limit') || '20');
const users = await db.user.findMany({
take: limit + 1,
...(cursor && { cursor: { id: cursor }, skip: 1 }),
orderBy: { id: 'asc' },
});
const hasMore = users.length > limit;
const data = hasMore ? users.slice(0, -1) : users;
return NextResponse.json({
data,
pagination: {
nextCursor: hasMore ? data[data.length - 1].id : null,
hasMore,
},
});
}Good - Offset-based (simpler, for admin panels):
typescript
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const page = parseInt(searchParams.get('page') || '1');
const limit = parseInt(searchParams.get('limit') || '20');
const [users, total] = await Promise.all([
db.user.findMany({
skip: (page - 1) * limit,
take: limit,
}),
db.user.count(),
]);
return NextResponse.json({
data: users,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
},
});
}Bad:
typescript
// ❌ No pagination - returns all records
export async function GET() {
const users = await db.user.findMany(); // Could be millions!
return NextResponse.json(users);
}Why: Pagination prevents performance issues and timeouts. Cursor-based is more efficient for large datasets, offset-based is simpler for small datasets.
适用场景:返回大型数据集时
正确示例 - 基于游标(推荐用于大型数据集):
typescript
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const cursor = searchParams.get('cursor');
const limit = parseInt(searchParams.get('limit') || '20');
const users = await db.user.findMany({
take: limit + 1,
...(cursor && { cursor: { id: cursor }, skip: 1 }),
orderBy: { id: 'asc' },
});
const hasMore = users.length > limit;
const data = hasMore ? users.slice(0, -1) : users;
return NextResponse.json({
data,
pagination: {
nextCursor: hasMore ? data[data.length - 1].id : null,
hasMore,
},
});
}正确示例 - 基于偏移量(实现简单,适用于管理面板):
typescript
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const page = parseInt(searchParams.get('page') || '1');
const limit = parseInt(searchParams.get('limit') || '20');
const [users, total] = await Promise.all([
db.user.findMany({
skip: (page - 1) * limit,
take: limit,
}),
db.user.count(),
]);
return NextResponse.json({
data: users,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
},
});
}错误示例:
typescript
// ❌ No pagination - returns all records
export async function GET() {
const users = await db.user.findMany(); // Could be millions!
return NextResponse.json(users);
}原因:分页可避免性能问题与超时。基于游标分页在大型数据集上更高效,基于偏移量分页实现更简单,适用于小型数据集。
Pattern 4: API Versioning
模式4:API版本控制
When: Making breaking changes to existing APIs
Good - URL versioning (most explicit):
GET /api/v1/users
GET /api/v2/userstypescript
// app/api/v1/users/route.ts
export async function GET() {
const users = await db.user.findMany();
return NextResponse.json(users); // Old format
}
// app/api/v2/users/route.ts
export async function GET() {
const users = await db.user.findMany({
include: { profile: true }, // New: Include related data
});
return NextResponse.json(users);
}Breaking vs Non-Breaking Changes:
typescript
// ✅ Non-breaking (no version bump needed):
// - Add new endpoint
// - Add optional field to request
// - Add new field to response
// ❌ Breaking (requires new version):
// - Remove endpoint
// - Remove field from response
// - Rename field
// - Change field type
// - Make optional field requiredWhy: Versioning allows backward compatibility while evolving the API. Existing clients continue working while new clients use improved versions.
适用场景:对现有API进行破坏性变更时
正确示例 - URL版本控制(最直观):
GET /api/v1/users
GET /api/v2/userstypescript
// app/api/v1/users/route.ts
export async function GET() {
const users = await db.user.findMany();
return NextResponse.json(users); // Old format
}
// app/api/v2/users/route.ts
export async function GET() {
const users = await db.user.findMany({
include: { profile: true }, // New: Include related data
});
return NextResponse.json(users);
}破坏性变更 vs 非破坏性变更:
typescript
// ✅ Non-breaking (no version bump needed):
// - Add new endpoint
// - Add optional field to request
// - Add new field to response
// ❌ Breaking (requires new version):
// - Remove endpoint
// - Remove field from response
// - Rename field
// - Change field type
// - Make optional field required原因:版本控制允许在演进API的同时保持向后兼容性。现有客户端可继续正常工作,新客户端则可使用改进后的版本。
Pattern 5: Consistent Error Handling
模式5:一致性错误处理
When: Handling errors and validation
Good:
typescript
interface ApiError {
code: string;
message: string;
details?: Array<{ field: string; message: string }>;
}
function errorResponse(status: number, error: ApiError) {
return NextResponse.json({ error }, { status });
}
export async function POST(request: Request) {
try {
const body = await request.json();
const errors = validateUser(body);
if (errors.length > 0) {
return errorResponse(400, {
code: 'VALIDATION_ERROR',
message: 'Invalid input data',
details: errors,
});
}
const user = await db.user.create({ data: body });
return NextResponse.json(user, { status: 201 });
} catch (error) {
if (error.code === 'P2002') {
return errorResponse(409, {
code: 'DUPLICATE_EMAIL',
message: 'Email already exists',
});
}
return errorResponse(500, {
code: 'INTERNAL_ERROR',
message: 'An unexpected error occurred',
});
}
}Bad:
typescript
// ❌ Inconsistent error formats
export async function POST(request: Request) {
try {
const body = await request.json();
if (!body.email) {
return NextResponse.json('Email required'); // Plain string
}
const user = await db.user.create({ data: body });
return NextResponse.json(user);
} catch (error) {
return NextResponse.json({
message: error.message, // Inconsistent format
stack: error.stack, // Leaks implementation details
});
}
}Why: Consistent error format makes client error handling predictable. Never expose stack traces or internal details in production.
适用场景:处理错误与验证时
正确示例:
typescript
interface ApiError {
code: string;
message: string;
details?: Array<{ field: string; message: string }>;
}
function errorResponse(status: number, error: ApiError) {
return NextResponse.json({ error }, { status });
}
export async function POST(request: Request) {
try {
const body = await request.json();
const errors = validateUser(body);
if (errors.length > 0) {
return errorResponse(400, {
code: 'VALIDATION_ERROR',
message: 'Invalid input data',
details: errors,
});
}
const user = await db.user.create({ data: body });
return NextResponse.json(user, { status: 201 });
} catch (error) {
if (error.code === 'P2002') {
return errorResponse(409, {
code: 'DUPLICATE_EMAIL',
message: 'Email already exists',
});
}
return errorResponse(500, {
code: 'INTERNAL_ERROR',
message: 'An unexpected error occurred',
});
}
}错误示例:
typescript
// ❌ Inconsistent error formats
export async function POST(request: Request) {
try {
const body = await request.json();
if (!body.email) {
return NextResponse.json('Email required'); // Plain string
}
const user = await db.user.create({ data: body });
return NextResponse.json(user);
} catch (error) {
return NextResponse.json({
message: error.message, // Inconsistent format
stack: error.stack, // Leaks implementation details
});
}
}原因:一致的错误格式让客户端的错误处理更具可预测性。生产环境中绝不要暴露堆栈跟踪或内部实现细节。
GraphQL Critical Patterns
GraphQL核心设计模式
Pattern 1: Schema Design with Types and Relationships
模式1:包含类型与关联关系的Schema设计
Good:
graphql
type User {
id: ID!
name: String!
email: String!
role: UserRole!
posts: [Post!]!
createdAt: DateTime!
}
enum UserRole {
ADMIN
USER
GUEST
}
type Post {
id: ID!
title: String!
content: String!
author: User!
published: Boolean!
}
type Query {
user(id: ID!): User
users(limit: Int, cursor: String): UserConnection!
}
type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User!
}
input CreateUserInput {
name: String!
email: String!
role: UserRole
}Why: Clear types, non-null fields (the suffix), and input types make the API self-documenting and type-safe.
!正确示例:
graphql
type User {
id: ID!
name: String!
email: String!
role: UserRole!
posts: [Post!]!
createdAt: DateTime!
}
enum UserRole {
ADMIN
USER
GUEST
}
type Post {
id: ID!
title: String!
content: String!
author: User!
published: Boolean!
}
type Query {
user(id: ID!): User
users(limit: Int, cursor: String): UserConnection!
}
type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User!
}
input CreateUserInput {
name: String!
email: String!
role: UserRole
}原因:清晰的类型定义、非空字段(后缀)以及输入类型让API具备自文档性且类型安全。
!Pattern 2: Solving N+1 Queries with DataLoader
模式2:使用DataLoader解决N+1查询问题
Problem:
typescript
// ❌ N+1 queries: 1 for users + N for posts
const resolvers = {
User: {
posts: async (parent, _args, context) => {
return context.db.post.findMany({
where: { authorId: parent.id },
});
},
},
};
// Querying 100 users = 101 database queries!Solution:
typescript
// ✅ DataLoader batches queries
import DataLoader from 'dataloader';
const postLoader = new DataLoader(async (userIds: readonly string[]) => {
const posts = await db.post.findMany({
where: { authorId: { in: [...userIds] } },
});
const postsByUserId = userIds.map(userId =>
posts.filter(post => post.authorId === userId)
);
return postsByUserId;
});
const resolvers = {
User: {
posts: (parent, _args, context) => {
return context.loaders.post.load(parent.id);
},
},
};
// Querying 100 users = 2 queries (users + batched posts)!Why: DataLoader batches and caches database queries, solving the N+1 problem and dramatically improving performance.
问题:
typescript
// ❌ N+1 queries: 1 for users + N for posts
const resolvers = {
User: {
posts: async (parent, _args, context) => {
return context.db.post.findMany({
where: { authorId: parent.id },
});
},
},
};
// Querying 100 users = 101 database queries!解决方案:
typescript
// ✅ DataLoader batches queries
import DataLoader from 'dataloader';
const postLoader = new DataLoader(async (userIds: readonly string[]) => {
const posts = await db.post.findMany({
where: { authorId: { in: [...userIds] } },
});
const postsByUserId = userIds.map(userId =>
posts.filter(post => post.authorId === userId)
);
return postsByUserId;
});
const resolvers = {
User: {
posts: (parent, _args, context) => {
return context.loaders.post.load(parent.id);
},
},
};
// Querying 100 users = 2 queries (users + batched posts)!原因:DataLoader可批量处理并缓存数据库查询,解决N+1查询问题并显著提升性能。
Anti-Patterns
反模式
❌ Anti-Pattern 1: Exposing Database Structure Directly
❌ 反模式1:直接暴露数据库结构
Don't do this:
typescript
// ❌ API mirrors database exactly
GET /api/users
Response: {
id: 123,
password_hash: "bcrypt...", // Exposing sensitive data!
created_at: "2024-01-15",
internal_notes: "VIP customer"
}Do this instead:
typescript
// ✅ API has its own contract
GET /api/users/123
Response: {
id: 123,
name: "Alice",
email: "alice@example.com",
role: "admin",
joinedAt: "2024-01-15T10:00:00Z"
}
// Server-side: Transform before sending
export async function GET(request: Request, { params }) {
const user = await db.user.findUnique({ where: { id: params.id } });
return NextResponse.json({
id: user.id,
name: user.name,
email: user.email,
role: user.role,
joinedAt: user.createdAt,
});
}请勿这样做:
typescript
// ❌ API mirrors database exactly
GET /api/users
Response: {
id: 123,
password_hash: "bcrypt...", // Exposing sensitive data!
created_at: "2024-01-15",
internal_notes: "VIP customer"
}正确做法:
typescript
// ✅ API has its own contract
GET /api/users/123
Response: {
id: 123,
name: "Alice",
email: "alice@example.com",
role: "admin",
joinedAt: "2024-01-15T10:00:00Z"
}
// Server-side: Transform before sending
export async function GET(request: Request, { params }) {
const user = await db.user.findUnique({ where: { id: params.id } });
return NextResponse.json({
id: user.id,
name: user.name,
email: user.email,
role: user.role,
joinedAt: user.createdAt,
});
}❌ Anti-Pattern 2: No Input Validation
❌ 反模式2:无输入验证
Don't do this:
typescript
// ❌ Trusting all input
export async function POST(request: Request) {
const body = await request.json();
const user = await db.user.create({ data: body });
return NextResponse.json(user);
}Do this instead:
typescript
// ✅ Validate all input
import { z } from 'zod';
const userSchema = z.object({
name: z.string().min(2).max(100),
email: z.string().email(),
age: z.number().int().min(18).max(120),
});
export async function POST(request: Request) {
const body = await request.json();
const result = userSchema.safeParse(body);
if (!result.success) {
return NextResponse.json(
{ error: result.error.format() },
{ status: 400 }
);
}
const user = await db.user.create({ data: result.data });
return NextResponse.json(user, { status: 201 });
}请勿这样做:
typescript
// ❌ Trusting all input
export async function POST(request: Request) {
const body = await request.json();
const user = await db.user.create({ data: body });
return NextResponse.json(user);
}正确做法:
typescript
// ✅ Validate all input
import { z } from 'zod';
const userSchema = z.object({
name: z.string().min(2).max(100),
email: z.string().email(),
age: z.number().int().min(18).max(120),
});
export async function POST(request: Request) {
const body = await request.json();
const result = userSchema.safeParse(body);
if (!result.success) {
return NextResponse.json(
{ error: result.error.format() },
{ status: 400 }
);
}
const user = await db.user.create({ data: result.data });
return NextResponse.json(user, { status: 201 });
}❌ Anti-Pattern 3: Ignoring Authentication
❌ 反模式3:忽略身份验证
Don't do this:
typescript
// ❌ No auth checks
export async function DELETE(request: Request) {
const { id } = await request.json();
await db.user.delete({ where: { id } });
return NextResponse.json({ success: true });
}Do this instead:
typescript
// ✅ Check authentication and authorization
export async function DELETE(request: Request) {
const session = await getSession(request);
if (!session) {
return new Response('Unauthorized', { status: 401 });
}
if (session.role !== 'ADMIN') {
return new Response('Forbidden', { status: 403 });
}
const { id } = await request.json();
await db.user.delete({ where: { id } });
return new Response(null, { status: 204 });
}请勿这样做:
typescript
// ❌ No auth checks
export async function DELETE(request: Request) {
const { id } = await request.json();
await db.user.delete({ where: { id } });
return NextResponse.json({ success: true });
}正确做法:
typescript
// ✅ Check authentication and authorization
export async function DELETE(request: Request) {
const session = await getSession(request);
if (!session) {
return new Response('Unauthorized', { status: 401 });
}
if (session.role !== 'ADMIN') {
return new Response('Forbidden', { status: 403 });
}
const { id } = await request.json();
await db.user.delete({ where: { id } });
return new Response(null, { status: 204 });
}Code Examples
代码示例
Example 1: RESTful CRUD Endpoint
示例1:RESTful CRUD端点
typescript
// app/api/users/[id]/route.ts
export async function GET(request: Request, { params }: { params: { id: string } }) {
const user = await db.user.findUnique({ where: { id: params.id } });
if (!user) {
return new Response('User not found', { status: 404 });
}
return NextResponse.json(user);
}
export async function PATCH(request: Request, { params }: { params: { id: string } }) {
const body = await request.json();
const user = await db.user.update({
where: { id: params.id },
data: body,
});
return NextResponse.json(user);
}typescript
// app/api/users/[id]/route.ts
export async function GET(request: Request, { params }: { params: { id: string } }) {
const user = await db.user.findUnique({ where: { id: params.id } });
if (!user) {
return new Response('User not found', { status: 404 });
}
return NextResponse.json(user);
}
export async function PATCH(request: Request, { params }: { params: { id: string } }) {
const body = await request.json();
const user = await db.user.update({
where: { id: params.id },
data: body,
});
return NextResponse.json(user);
}Example 2: GraphQL Resolver with DataLoader
示例2:带DataLoader的GraphQL解析器
typescript
const resolvers = {
Query: {
users: async (_parent, { limit = 20, cursor }, context) => {
const users = await context.db.user.findMany({
take: limit + 1,
...(cursor && { cursor: { id: cursor }, skip: 1 }),
});
const hasMore = users.length > limit;
const data = hasMore ? users.slice(0, -1) : users;
return {
edges: data.map(user => ({ node: user, cursor: user.id })),
pageInfo: {
hasNextPage: hasMore,
endCursor: hasMore ? data[data.length - 1].id : null,
},
};
},
},
User: {
posts: (parent, _args, context) => {
return context.loaders.posts.load(parent.id);
},
},
};For comprehensive examples and detailed implementations, see the references/ folder.
typescript
const resolvers = {
Query: {
users: async (_parent, { limit = 20, cursor }, context) => {
const users = await context.db.user.findMany({
take: limit + 1,
...(cursor && { cursor: { id: cursor }, skip: 1 }),
});
const hasMore = users.length > limit;
const data = hasMore ? users.slice(0, -1) : users;
return {
edges: data.map(user => ({ node: user, cursor: user.id })),
pageInfo: {
hasNextPage: hasMore,
endCursor: hasMore ? data[data.length - 1].id : null,
},
};
},
},
User: {
posts: (parent, _args, context) => {
return context.loaders.posts.load(parent.id);
},
},
};如需完整示例与详细实现,请查看references/文件夹。
Quick Reference
快速检查清单
REST Checklist
REST检查清单
- Use appropriate HTTP methods (GET, POST, PUT, PATCH, DELETE)
- Return correct status codes (2xx, 4xx, 5xx)
- Use nouns for resources, not verbs
- Implement pagination for collections
- Version API for breaking changes
- Validate all input
- Return consistent error format
- Add authentication/authorization checks
- 使用合适的HTTP方法(GET、POST、PUT、PATCH、DELETE)
- 返回正确的状态码(2xx、4xx、5xx)
- 资源使用名词而非动词
- 为数据集实现分页
- 对破坏性变更进行API版本控制
- 验证所有输入
- 返回一致的错误格式
- 添加身份验证/授权检查
GraphQL Checklist
GraphQL检查清单
- Design clear schema with types and relationships
- Use DataLoader to prevent N+1 queries
- Validate inputs with Zod or similar
- Return meaningful error codes in extensions
- Implement authentication via context
- Use connection pattern for pagination
- Keep mutations simple and focused
- 设计清晰的Schema,包含类型与关联关系
- 使用DataLoader防止N+1查询
- 使用Zod或类似工具验证输入
- 在扩展字段中返回有意义的错误码
- 通过上下文实现身份验证
- 使用连接模式进行分页
- 保持变更操作简单且聚焦
Progressive Disclosure
进阶内容
For detailed implementations, see:
- REST Patterns - Pagination, filtering, versioning, rate limiting, HATEOAS
- GraphQL Design - Resolvers, DataLoader, subscriptions, directives, input validation
如需详细实现指南,请查看:
- REST模式参考 - 分页、过滤、版本控制、速率限制、HATEOAS
- GraphQL设计指南 - 解析器、DataLoader、订阅、指令、输入验证
References
参考资料
- REST Patterns Reference
- GraphQL Design Reference
- GraphQL Documentation
- REST API Tutorial
Maintained by dsmj-ai-toolkit