mcpField
B
backend-api-patterns

backend-api-patterns

github.com/duyet开发1569 次安装1 周前更新
代码仓库

关于 backend-api-patterns

为后端服务和 API 提供实现模式的技能,适用于处理端点、数据访问、身份验证、缓存和服务架构的开发者。

主要功能

  1. 01统一的 API 响应封装
  2. 02路由与服务层模式
  3. 03JWT 和中间件身份验证
  4. 04数据库事务与查询优化
  5. 05缓存和请求去重
  6. 06输入校验与结构化日志

适用场景

  1. 01按统一错误格式实现 API 端点
  2. 02拆分路由、服务和仓储逻辑
  3. 03优化关联记录查询
  4. 04添加缓存或合并并发请求

安装

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

$npx skills add https://github.com/duyet/codex-claude-plugins --skill backend-api-patterns

安装后在 Claude Code 中输入/backend-api-patterns确认已加载。

详细介绍

此技能提供后端和 API 实现模式,帮助构建稳健、可扩展的服务。

何时调用此技能

在以下情况下自动启用:

  • 实现 API 端点
  • 数据库操作和查询
  • 身份验证和授权
  • 缓存和性能优化
  • 服务架构设计

API 设计模式

一致的响应结构
// Standard API response envelope
interface ApiResponse<T> {
  data?: T;
  error?: {
    code: string;
    message: string;
    details?: Record<string, unknown>;
  };
  meta?: {
    pagination?: {
      page: number;
      pageSize: number;
      total: number;
      totalPages: number;
    };
    timestamp?: string;
    requestId?: string;
  };
}

// Success response helper
function success<T>(data: T, meta?: ApiResponse<T>['meta']): ApiResponse<T> {
  return { data, meta };
}

// Error response helper
function error(
  code: string,
  message: string,
  details?: Record<string, unknown>
): ApiResponse<never> {
  return { error: { code, message, details } };
}

// Paginated response helper
function paginated<T>(
  data: T[],
  page: number,
  pageSize: number,
  total: number
): ApiResponse<T[]> {
  return {
    data,
    meta: {
      pagination: {
        page,
        pageSize,
        total,
        totalPages: Math.ceil(total / pageSize),
      },
    },
  };
}
路由处理器模式
// Generic handler wrapper with error handling
type Handler<T> = (
  req: Request,
  context: { params: Record<string, string> }
) => Promise<T>;

function createHandler<T>(handler: Handler<T>) {
  return async (req: Request, context: { params: Record<string, string> }) => {
    const requestId = crypto.randomUUID();

    try {
      const result = await handler(req, context);
      return Response.json(success(result, { requestId }));
    } catch (err) {
      if (err instanceof AppError) {
        return Response.json(
          error(err.code, err.message),
          { status: err.statusCode }
        );
      }

      console.error(`[${requestId}] Unexpected error:`, err);
      return Response.json(
        error('INTERNAL_ERROR', 'An unexpected error occurred'),
        { status: 500 }
      );
    }
  };
}

// Usage
export const GET = createHandler(async (req, { params }) => {
  const user = await userService.findById(params.id);
  if (!user) throw new NotFoundError('User', params.id);
  return user;
});

服务层模式

仓储模式
interface Repository<T, ID = string> {
  findById(id: ID): Promise<T | null>;
  findMany(options: FindOptions<T>): Promise<T[]>;
  count(filter?: Partial<T>): Promise<number>;
  create(data: CreateInput<T>): Promise<T>;
  update(id: ID, data: UpdateInput<T>): Promise<T>;
  delete(id: ID): Promise<void>;
}

interface FindOptions<T> {
  filter?: Partial<T>;
  orderBy?: keyof T;
  orderDir?: 'asc' | 'desc';
  limit?: number;
  offset?: number;
}

type CreateInput<T> = Omit<T, 'id' | 'createdAt' | 'updatedAt'>;
type UpdateInput<T> = Partial<Omit<T, 'id' | 'createdAt' | 'updatedAt'>>;

// Implementation
class UserRepository implements Repository<User> {
  constructor(private db: Database) {}

  async findById(id: string): Promise<User | null> {
    return this.db.query.users.findFirst({
      where: eq(users.id, id),
    });
  }

  async findMany(options: FindOptions<User>): Promise<User[]> {
    const { filter, orderBy, orderDir = 'asc', limit, offset } = options;

    return this.db.query.users.findMany({
      where: filter ? this.buildWhere(filter) : undefined,
      orderBy: orderBy ? (orderDir === 'asc' ? asc : desc)(users[orderBy]) : undefined,
      limit,
      offset,
    });
  }

  // ... other methods
}
包含业务逻辑的服务
class UserService {
  constructor(
    private userRepo: Repository<User>,
    private cache: Cache,
    private eventBus: EventBus
  ) {}

  async getUser(id: string): Promise<User> {
    // Check cache first
    const cached = await this.cache.get<User>(`user:${id}`);
    if (cached) return cached;

    // Fetch from database
    const user = await this.userRepo.findById(id);
    if (!user) throw new NotFoundError('User', id);

    // Cache for future requests
    await this.cache.set(`user:${id}`, user, { ttl: 3600 });

    return user;
  }

  async createUser(input: CreateUserInput): Promise<User> {
    // Validate
    const existing = await this.userRepo.findMany({
      filter: { email: input.email },
      limit: 1,
    });
    if (existing.length > 0) {
      throw new ValidationError('Email already exists', { email: 'Already in use' });
    }

    // Hash password
    const hashedPassword = await hashPassword(input.password);

    // Create user
    const user = await this.userRepo.create({
      ...input,
      password: hashedPassword,
    });

    // Emit event for side effects
    await this.eventBus.emit('user.created', { userId: user.id });

    return user;
  }

  async updateUser(id: string, input: UpdateUserInput): Promise<User> {
    const user = await this.userRepo.update(id, input);

    // Invalidate cache
    await this.cache.delete(`user:${id}`);

    return user;
  }
}

身份验证模式

带刷新令牌的 JWT
interface TokenPair {
  accessToken: string;   // Short-lived: 15 minutes
  refreshToken: string;  // Long-lived: 7 days
}

interface TokenPayload {
  sub: string;           // User ID
  email: string;
  roles: string[];
  type: 'access' | 'refresh';
}

class AuthService {
  constructor(
    private userRepo: Repository<User>,
    private tokenRepo: Repository<RefreshToken>,
    private jwtSecret: string
  ) {}

  async login(email: string, password: string): Promise<TokenPair> {
    const user = await this.userRepo.findMany({
      filter: { email },
      limit: 1,
    });

    if (!user[0] || !await verifyPassword(password, user[0].password)) {
      throw new UnauthorizedError('Invalid credentials');
    }

    return this.generateTokenPair(user[0]);
  }

  async refresh(refreshToken: string): Promise<TokenPair> {
    // Verify token
    const payload = this.verifyToken(refreshToken);
    if (payload.type !== 'refresh') {
      throw new UnauthorizedError('Invalid token type');
    }

    // Check if token is revoked
    const stored = await this.tokenRepo.findById(refreshToken);
    if (!stored || stored.revoked) {
      throw new UnauthorizedError('Token revoked');
    }

    // Get user and generate new tokens
    const user = await this.userRepo.findById(payload.sub);
    if (!user) throw new UnauthorizedError('User not found');

    // Revoke old refresh token
    await this.tokenRepo.update(refreshToken, { revoked: true });

    return this.generateTokenPair(user);
  }

  private generateTokenPair(user: User): TokenPair {
    const accessToken = jwt.sign(
      { sub: user.id, email: user.email, roles: user.roles, type: 'access' },
      this.jwtSecret,
      { expiresIn: '15m' }
    );

    const refreshToken = jwt.sign(
      { sub: user.id, type: 'refresh' },
      this.jwtSecret,
      { expiresIn: '7d' }
    );

    return { accessToken, refreshToken };
  }

  private verifyToken(token: string): TokenPayload {
    try {
      return jwt.verify(token, this.jwtSecret) as TokenPayload;
    } catch {
      throw new UnauthorizedError('Invalid or expired token');
    }
  }
}
中间件模式
type Middleware = (req: Request, next: () => Promise<Response>) => Promise<Response>;

// Auth middleware
function authMiddleware(requiredRoles?: string[]): Middleware {
  return async (req, next) => {
    const token = req.headers.get('Authorization')?.replace('Bearer ', '');

    if (!token) {
      return Response.json(
        error('UNAUTHORIZED', 'No token provided'),
        { status: 401 }
      );
    }

    try {
      const payload = verifyToken(token);

      if (requiredRoles?.length && !requiredRoles.some(r => payload.roles.includes(r))) {
        return Response.json(
          error('FORBIDDEN', 'Insufficient permissions'),
          { status: 403 }
        );
      }

      // Attach user to request context
      (req as any).user = payload;

      return next();
    } catch {
      return Response.json(
        error('UNAUTHORIZED', 'Invalid or expired token'),
        { status: 401 }
      );
    }
  };
}

// Rate limiting middleware
function rateLimitMiddleware(limit: number, windowMs: number): Middleware {
  const requests = new Map<string, { count: number; resetAt: number }>();

  return async (req, next) => {
    const ip = req.headers.get('x-forwarded-for') || 'unknown';
    const now = Date.now();

    const record = requests.get(ip);

    if (!record || record.resetAt < now) {
      requests.set(ip, { count: 1, resetAt: now + windowMs });
      return next();
    }

    if (record.count >= limit) {
      return Response.json(
        error('RATE_LIMITED', 'Too many requests'),
        { status: 429 }
      );
    }

    record.count++;
    return next();
  };
}

数据库模式

查询优化
// Avoid N+1 queries with eager loading
async function getUsersWithOrders(): Promise<UserWithOrders[]> {
  // BAD: N+1 queries
  const users = await db.query.users.findMany();
  for (const user of users) {
    user.orders = await db.query.orders.findMany({
      where: eq(orders.userId, user.id),
    });
  }

  // GOOD: Single query with join
  return db.query.users.findMany({
    with: {
      orders: true,
    },
  });
}

// Pagination with cursor
async function paginateUsers(cursor?: string, limit = 20): Promise<{
  users: User[];
  nextCursor: string | null;
}> {
  const users = await db.query.users.findMany({
    where: cursor ? gt(users.id, cursor) : undefined,
    orderBy: asc(users.id),
    limit: limit + 1, // Fetch one extra to check for next page
  });

  const hasMore = users.length > limit;
  const data = hasMore ? users.slice(0, -1) : users;

  return {
    users: data,
    nextCursor: hasMore ? data[data.length - 1].id : null,
  };
}
事务模式
async function transferFunds(
  fromId: string,
  toId: string,
  amount: number
): Promise<void> {
  await db.transaction(async (tx) => {
    // Lock rows for update
    const from = await tx.query.accounts.findFirst({
      where: eq(accounts.id, fromId),
      for: 'update',
    });

    if (!from || from.balance < amount) {
      throw new ValidationError('Insufficient funds', {});
    }

    // Debit source account
    await tx.update(accounts)
      .set({ balance: from.balance - amount })
      .where(eq(accounts.id, fromId));

    // Credit destination account
    await tx.update(accounts)
      .set({ balance: sql`${accounts.balance} + ${amount}` })
      .where(eq(accounts.id, toId));

    // Log transaction
    await tx.insert(transactions).values({
      fromId,
      toId,
      amount,
      type: 'transfer',
    });
  });
}

缓存模式

Cache-Aside 模式
class CachedUserService {
  constructor(
    private userRepo: Repository<User>,
    private cache: Cache
  ) {}

  async getUser(id: string): Promise<User | null> {
    const cacheKey = `user:${id}`;

    // Try cache first
    const cached = await this.cache.get<User>(cacheKey);
    if (cached) return cached;

    // Fetch from database
    const user = await this.userRepo.findById(id);

    // Cache the result (including null to prevent cache stampede)
    if (user) {
      await this.cache.set(cacheKey, user, { ttl: 3600 });
    } else {
      await this.cache.set(cacheKey, null, { ttl: 60 }); // Short TTL for negative cache
    }

    return user;
  }

  async updateUser(id: string, data: UpdateUserInput): Promise<User> {
    const user = await this.userRepo.update(id, data);

    // Invalidate cache
    await this.cache.delete(`user:${id}`);

    return user;
  }
}
请求去重
class RequestDeduplicator {
  private pending = new Map<string, Promise<unknown>>();

  async dedupe<T>(key: string, fetcher: () => Promise<T>): Promise<T> {
    // Return existing request if in flight
    const existing = this.pending.get(key);
    if (existing) return existing as Promise<T>;

    // Start new request
    const promise = fetcher().finally(() => {
      this.pending.delete(key);
    });

    this.pending.set(key, promise);
    return promise;
  }
}

// Usage
const deduplicator = new RequestDeduplicator();

async function getUser(id: string): Promise<User> {
  return deduplicator.dedupe(`user:${id}`, () => userRepo.findById(id));
}

最佳实践检查清单

  • 使用一致的 API 响应封装
  • 实现合理的错误层级和处理机制
  • 分离关注点:路由 → 服务 → 仓储
  • 对多步骤操作使用事务
  • 实现缓存并进行合理失效处理
  • 使用预加载避免 N+1 查询
  • 对大型数据集使用基于游标的分页
  • 实现速率限制和请求去重
  • 在 API 边界验证输入
  • 使用结构化数据和请求 ID 记录日志

评论

支持 Markdown