Back to list
Wania-Kazmi

api-patterns

by Wania-Kazmi

Autonomous project generator for Claude Code. Write requirements, run one command, get a complete project with custom skills, agents, hooks, TDD, 80%+ coverage, and security-reviewed code.

2🍴 0📅 Jan 24, 2026

SKILL.md


name: api-patterns description: | REST API and GraphQL design patterns, authentication, error handling, and best practices. Use when building APIs, designing endpoints, or implementing backend services. allowed-tools: Read, Write, Edit, Bash, Grep, Glob

API Design Patterns & Best Practices

REST API Principles

1. Resource-Based URLs

# GOOD: Resource-oriented
GET    /users           # List users
GET    /users/123       # Get specific user
POST   /users           # Create user
PUT    /users/123       # Update user (full)
PATCH  /users/123       # Update user (partial)
DELETE /users/123       # Delete user

# BAD: Action-oriented
GET    /getUsers
POST   /createUser
POST   /updateUser/123

2. Use Proper HTTP Methods

  • GET: Read (idempotent, cacheable)
  • POST: Create (not idempotent)
  • PUT: Replace (idempotent)
  • PATCH: Partial update (not idempotent)
  • DELETE: Remove (idempotent)

3. Use Proper HTTP Status Codes

// Success
200 OK           // General success
201 Created      // Resource created (POST)
204 No Content   // Success with no body (DELETE)

// Client Errors
400 Bad Request  // Invalid input
401 Unauthorized // No/invalid auth
403 Forbidden    // Auth valid but not allowed
404 Not Found    // Resource doesn't exist
409 Conflict     // Resource state conflict
422 Unprocessable // Validation failed

// Server Errors
500 Internal     // Unexpected error
503 Unavailable  // Service temporarily down

Response Format

Standard Response Structure

interface ApiResponse<T> {
  success: boolean
  data?: T
  error?: {
    code: string
    message: string
    details?: Record<string, string[]>
  }
  meta?: {
    total: number
    page: number
    limit: number
    hasMore: boolean
  }
}

// Success response
{
  "success": true,
  "data": {
    "id": "123",
    "name": "John Doe",
    "email": "john@example.com"
  }
}

// List response with pagination
{
  "success": true,
  "data": [
    { "id": "1", "name": "User 1" },
    { "id": "2", "name": "User 2" }
  ],
  "meta": {
    "total": 100,
    "page": 1,
    "limit": 20,
    "hasMore": true
  }
}

// Error response
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid input data",
    "details": {
      "email": ["Invalid email format"],
      "age": ["Must be at least 18"]
    }
  }
}

Request Validation

Zod Schema Validation

import { z } from 'zod'

const CreateUserSchema = z.object({
  name: z.string().min(1).max(100),
  email: z.string().email(),
  age: z.number().int().min(18).max(150).optional(),
  role: z.enum(['user', 'admin']).default('user')
})

type CreateUserInput = z.infer<typeof CreateUserSchema>

// Usage in endpoint
export async function POST(request: Request) {
  try {
    const body = await request.json()
    const data = CreateUserSchema.parse(body)
    
    const user = await createUser(data)
    return Response.json({ success: true, data: user }, { status: 201 })
  } catch (error) {
    if (error instanceof z.ZodError) {
      return Response.json({
        success: false,
        error: {
          code: 'VALIDATION_ERROR',
          message: 'Invalid input',
          details: formatZodErrors(error)
        }
      }, { status: 422 })
    }
    throw error
  }
}

Error Handling

Custom Error Classes

export class AppError extends Error {
  constructor(
    public message: string,
    public statusCode: number = 500,
    public code: string = 'INTERNAL_ERROR'
  ) {
    super(message)
    this.name = 'AppError'
  }
}

export class NotFoundError extends AppError {
  constructor(resource: string) {
    super(`${resource} not found`, 404, 'NOT_FOUND')
  }
}

export class UnauthorizedError extends AppError {
  constructor(message = 'Unauthorized') {
    super(message, 401, 'UNAUTHORIZED')
  }
}

export class ValidationError extends AppError {
  constructor(
    message: string,
    public details?: Record<string, string[]>
  ) {
    super(message, 422, 'VALIDATION_ERROR')
  }
}

// Usage
throw new NotFoundError('User')
throw new UnauthorizedError('Invalid token')
throw new ValidationError('Invalid input', { email: ['Required'] })

Global Error Handler

// Express middleware
export function errorHandler(
  error: Error,
  req: Request,
  res: Response,
  next: NextFunction
) {
  console.error('Error:', error)

  if (error instanceof AppError) {
    return res.status(error.statusCode).json({
      success: false,
      error: {
        code: error.code,
        message: error.message,
        details: error instanceof ValidationError ? error.details : undefined
      }
    })
  }

  // Don't leak internal errors
  return res.status(500).json({
    success: false,
    error: {
      code: 'INTERNAL_ERROR',
      message: 'An unexpected error occurred'
    }
  })
}

Authentication Patterns

JWT Authentication

import jwt from 'jsonwebtoken'

const JWT_SECRET = process.env.JWT_SECRET!
const JWT_EXPIRES_IN = '7d'

export function generateToken(user: User): string {
  return jwt.sign(
    { userId: user.id, role: user.role },
    JWT_SECRET,
    { expiresIn: JWT_EXPIRES_IN }
  )
}

export function verifyToken(token: string): JwtPayload {
  try {
    return jwt.verify(token, JWT_SECRET) as JwtPayload
  } catch {
    throw new UnauthorizedError('Invalid token')
  }
}

// Auth middleware
export async function authMiddleware(req: Request) {
  const authHeader = req.headers.get('authorization')
  
  if (!authHeader?.startsWith('Bearer ')) {
    throw new UnauthorizedError('Missing token')
  }

  const token = authHeader.slice(7)
  const payload = verifyToken(token)
  
  return payload
}

API Key Authentication

export async function apiKeyAuth(req: Request) {
  const apiKey = req.headers.get('x-api-key')
  
  if (!apiKey) {
    throw new UnauthorizedError('Missing API key')
  }

  const hashedKey = hashApiKey(apiKey)
  const keyRecord = await db.apiKeys.findUnique({
    where: { hashedKey }
  })

  if (!keyRecord || keyRecord.revokedAt) {
    throw new UnauthorizedError('Invalid API key')
  }

  // Update last used
  await db.apiKeys.update({
    where: { id: keyRecord.id },
    data: { lastUsedAt: new Date() }
  })

  return keyRecord
}

Rate Limiting

Token Bucket Implementation

import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'

const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(100, '1 m'), // 100 requests per minute
  analytics: true
})

export async function rateLimitMiddleware(req: Request) {
  const ip = req.headers.get('x-forwarded-for') ?? 'anonymous'
  const { success, limit, remaining, reset } = await ratelimit.limit(ip)

  if (!success) {
    return Response.json(
      {
        success: false,
        error: {
          code: 'RATE_LIMITED',
          message: 'Too many requests'
        }
      },
      {
        status: 429,
        headers: {
          'X-RateLimit-Limit': limit.toString(),
          'X-RateLimit-Remaining': remaining.toString(),
          'X-RateLimit-Reset': reset.toString()
        }
      }
    )
  }
}

Pagination

interface PaginationParams {
  cursor?: string
  limit?: number
}

async function getUsers(params: PaginationParams) {
  const limit = Math.min(params.limit ?? 20, 100)
  
  const users = await db.users.findMany({
    take: limit + 1, // Fetch one extra to check hasMore
    cursor: params.cursor ? { id: params.cursor } : undefined,
    orderBy: { createdAt: 'desc' }
  })

  const hasMore = users.length > limit
  const data = hasMore ? users.slice(0, -1) : users
  const nextCursor = hasMore ? data[data.length - 1].id : null

  return {
    data,
    meta: {
      hasMore,
      nextCursor
    }
  }
}

Offset-Based

async function getUsers(page: number = 1, limit: number = 20) {
  const offset = (page - 1) * limit
  
  const [users, total] = await Promise.all([
    db.users.findMany({ skip: offset, take: limit }),
    db.users.count()
  ])

  return {
    data: users,
    meta: {
      total,
      page,
      limit,
      totalPages: Math.ceil(total / limit),
      hasMore: offset + users.length < total
    }
  }
}

Versioning

/api/v1/users
/api/v2/users

Header Versioning

const version = req.headers.get('api-version') ?? 'v1'

switch (version) {
  case 'v1':
    return handleV1(req)
  case 'v2':
    return handleV2(req)
  default:
    throw new AppError('Unsupported API version', 400)
}

CORS Configuration

const corsHeaders = {
  'Access-Control-Allow-Origin': process.env.ALLOWED_ORIGINS ?? '*',
  'Access-Control-Allow-Methods': 'GET, POST, PUT, PATCH, DELETE, OPTIONS',
  'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-API-Key',
  'Access-Control-Max-Age': '86400' // 24 hours
}

// Handle preflight
export function OPTIONS() {
  return new Response(null, { headers: corsHeaders })
}

// Add to responses
export function withCors(response: Response) {
  Object.entries(corsHeaders).forEach(([key, value]) => {
    response.headers.set(key, value)
  })
  return response
}

Security Headers

const securityHeaders = {
  'X-Content-Type-Options': 'nosniff',
  'X-Frame-Options': 'DENY',
  'X-XSS-Protection': '1; mode=block',
  'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
  'Content-Security-Policy': "default-src 'self'",
  'Referrer-Policy': 'strict-origin-when-cross-origin'
}

OpenAPI Documentation

/**
 * @openapi
 * /api/users:
 *   get:
 *     summary: List users
 *     tags: [Users]
 *     parameters:
 *       - in: query
 *         name: page
 *         schema:
 *           type: integer
 *         description: Page number
 *     responses:
 *       200:
 *         description: List of users
 *         content:
 *           application/json:
 *             schema:
 *               $ref: '#/components/schemas/UsersResponse'
 */

GraphQL Patterns

Schema Design

type User {
  id: ID!
  name: String!
  email: String!
  posts: [Post!]!
  createdAt: DateTime!
}

type Query {
  user(id: ID!): User
  users(first: Int, after: String): UserConnection!
}

type Mutation {
  createUser(input: CreateUserInput!): User!
  updateUser(id: ID!, input: UpdateUserInput!): User!
  deleteUser(id: ID!): Boolean!
}

input CreateUserInput {
  name: String!
  email: String!
}

Resolver Pattern

const resolvers = {
  Query: {
    user: async (_, { id }, context) => {
      return context.dataSources.users.findById(id)
    },
    users: async (_, { first, after }, context) => {
      return context.dataSources.users.findMany({ first, after })
    }
  },
  User: {
    posts: async (user, _, context) => {
      return context.dataSources.posts.findByUserId(user.id)
    }
  }
}

Checklist

  • RESTful resource naming
  • Proper HTTP methods and status codes
  • Consistent response format
  • Input validation (Zod)
  • Error handling (custom errors)
  • Authentication (JWT/API key)
  • Rate limiting
  • Pagination (cursor-based)
  • CORS configured
  • Security headers set
  • API documentation (OpenAPI)

Score

Total Score

65/100

Based on repository quality metrics

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

0/10
説明文

100文字以上の説明がある

+10
人気

GitHub Stars 100以上

0/15
最近の活動

1ヶ月以内に更新

+10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

+5

Reviews

💬

Reviews coming soon