backend-api-patterns

📁 duyet/claude-plugins 📅 7 days ago
8
总安装量
2
周安装量
#33969
全站排名
安装命令
npx skills add https://github.com/duyet/claude-plugins --skill backend-api-patterns

Agent 安装分布

openclaw 2
opencode 2
kimi-cli 2
codex 2
gemini-cli 2
kilo 1

Skill 文档

This skill provides backend and API implementation patterns for building robust, scalable services.

When to Invoke This Skill

Automatically activate for:

  • API endpoint implementation
  • Database operations and queries
  • Authentication and authorization
  • Caching and performance optimization
  • Service architecture design

API Design Patterns

Consistent Response Structure

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

Route Handler Pattern

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

Service Layer Pattern

Repository Pattern

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
}

Service with Business Logic

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

Authentication Patterns

JWT with Refresh Tokens

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

Middleware Pattern

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

Database Patterns

Query Optimization

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

Transaction Pattern

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

Caching Patterns

Cache-Aside Pattern

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

Request Deduplication

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

Best Practices Checklist

  • Use consistent API response envelope
  • Implement proper error hierarchy and handling
  • Separate concerns: routes → services → repositories
  • Use transactions for multi-step operations
  • Implement caching with proper invalidation
  • Avoid N+1 queries with eager loading
  • Use cursor-based pagination for large datasets
  • Implement rate limiting and request deduplication
  • Validate inputs at API boundaries
  • Log with structured data and request IDs