API Design Skill
Systematic methodology for designing, reviewing, and improving APIs following industry best practices.
When to Use
- Designing new API endpoints
- Reviewing existing API architecture
- Creating data contracts between services
- Standardizing API patterns across a codebase
- Migrating or versioning APIs
CRITICAL: Check Existing First
Before creating ANY new endpoint, verify:
- Search for existing endpoints:
rg "export.*GET|POST|PATCH|DELETE" app/api/ --type ts
ls -la app/api/
- Check for existing Server Actions:
rg "'use server'" --type ts -l
rg "export async function" src/features/*/server/
- Check existing RPC functions (Supabase):
SELECT proname FROM pg_proc WHERE pronamespace = 'public'::regnamespace;
- Review existing patterns:
- Check
app/api/folder structure - Look at existing error handling patterns
- Verify authentication patterns used
Why: Duplicate endpoints cause routing conflicts, inconsistent behavior, and maintenance nightmares. Always search before creating.
API Design Principles
1. RESTful Resource Design
URL Structure:
GET /resources → List resources
GET /resources/:id → Get single resource
POST /resources → Create resource
PATCH /resources/:id → Partial update
PUT /resources/:id → Full replace
DELETE /resources/:id → Delete resource
# Nested resources
GET /users/:id/posts → User's posts
POST /users/:id/posts → Create post for user
# Actions (when CRUD doesn't fit)
POST /orders/:id/cancel → Cancel order
POST /users/:id/verify → Verify user
Naming Conventions:
- Use plural nouns for resources (
/users, not/user) - Use kebab-case for multi-word (
/user-profiles) - Avoid verbs in URLs (let HTTP methods convey action)
- Keep URLs shallow (max 2-3 levels deep)
2. Request/Response Design
Request Structure:
// Query params for filtering/sorting/pagination
GET /products?category=electronics&sort=-price&page=2&limit=20
// Body for mutations
POST /products
{
"name": "Widget",
"price": 29.99,
"categoryId": "cat_123"
}
// Partial updates with PATCH
PATCH /products/123
{
"price": 24.99 // Only changed fields
}
Response Structure (Consistent Envelope):
// Success response
{
"data": { /* resource or array */ },
"meta": {
"pagination": { "page": 1, "limit": 20, "total": 100 }
}
}
// Error response
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid input",
"details": [
{ "field": "email", "message": "Invalid email format" }
]
}
}
3. HTTP Status Codes
| Code | Meaning | When to Use | |------|---------|-------------| | 200 | OK | Successful GET, PATCH, PUT | | 201 | Created | Successful POST | | 204 | No Content | Successful DELETE | | 400 | Bad Request | Invalid input, validation error | | 401 | Unauthorized | Missing/invalid auth | | 403 | Forbidden | Auth valid but no permission | | 404 | Not Found | Resource doesn't exist | | 409 | Conflict | Duplicate, state conflict | | 422 | Unprocessable | Semantic validation error | | 429 | Too Many Requests | Rate limited | | 500 | Internal Error | Server error (never expose details) |
4. Pagination Patterns
Offset-based (simple but slow at scale):
GET /posts?page=5&limit=20
{
"data": [...],
"meta": {
"pagination": {
"page": 5,
"limit": 20,
"total": 543,
"totalPages": 28
}
}
}
Cursor-based (better for large datasets):
GET /posts?cursor=eyJpZCI6MTIzfQ&limit=20
{
"data": [...],
"meta": {
"pagination": {
"nextCursor": "eyJpZCI6MTQzfQ",
"prevCursor": "eyJpZCI6MTAzfQ",
"hasMore": true
}
}
}
5. Filtering & Sorting
// Filtering
GET /products?status=active&minPrice=10&maxPrice=100
GET /products?category[in]=electronics,books
GET /products?createdAt[gte]=2024-01-01
// Sorting
GET /products?sort=price // Ascending
GET /products?sort=-price // Descending
GET /products?sort=-createdAt,name // Multiple
// Field selection (reduce payload)
GET /users?fields=id,name,email
6. Versioning Strategies
URL versioning (recommended for breaking changes):
/api/v1/users
/api/v2/users
Header versioning (cleaner URLs):
Accept: application/vnd.api+json; version=2
When to version:
- Breaking changes to response structure
- Removing fields or endpoints
- Changing field types
- NOT for additive changes (new optional fields)
7. Authentication Patterns
Bearer Token (JWT):
headers: {
'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIs...'
}
API Key (service-to-service):
headers: {
'X-API-Key': 'sk_live_abc123...'
}
Security Checklist:
- [ ] Auth on every endpoint (except public)
- [ ] Rate limiting per user/IP
- [ ] Request validation before processing
- [ ] No sensitive data in URLs (use headers/body)
- [ ] HTTPS only
8. Error Handling
Consistent Error Schema:
interface ApiError {
error: {
code: string // Machine-readable (VALIDATION_ERROR)
message: string // Human-readable
details?: ErrorDetail[] // Field-level errors
requestId?: string // For debugging
}
}
interface ErrorDetail {
field: string
message: string
code?: string
}
Implementation:
// Zod validation errors → API errors
function formatZodError(error: ZodError): ApiError {
return {
error: {
code: 'VALIDATION_ERROR',
message: 'Invalid request data',
details: error.errors.map(e => ({
field: e.path.join('.'),
message: e.message,
}))
}
}
}
9. Next.js Route Handler Patterns
// app/api/products/route.ts
import { z } from 'zod'
import { NextRequest, NextResponse } from 'next/server'
const CreateProductSchema = z.object({
name: z.string().min(1).max(200),
price: z.number().positive(),
})
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const validated = CreateProductSchema.parse(body)
const product = await db.product.create({ data: validated })
return NextResponse.json(
{ data: product },
{ status: 201 }
)
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
formatZodError(error),
{ status: 400 }
)
}
return NextResponse.json(
{ error: { code: 'INTERNAL_ERROR', message: 'Something went wrong' } },
{ status: 500 }
)
}
}
10. Server Actions vs Route Handlers (Next.js 15+)
| Use Case | Recommendation |
|----------|----------------|
| Internal mutations | Server Actions |
| Webhooks from external services | Route Handlers |
| Third-party API proxy | Route Handlers |
| File uploads | Route Handlers (or Server Actions with unstable_after) |
| Streaming responses | Route Handlers |
| Form submissions | Server Actions |
| Background tasks | Server Actions + after() from next/server |
Server Action Example (Next.js 15):
// app/actions.ts
'use server'
import { revalidatePath } from 'next/cache'
import { after } from 'next/server'
export async function createProduct(formData: FormData) {
const product = await db.product.create({
data: { name: formData.get('name') }
})
// Run after response (Next.js 15)
after(async () => {
await sendNotification(product.id)
})
revalidatePath('/products')
return { success: true, data: product }
}
API Review Checklist
- [ ] URLs follow RESTful conventions
- [ ] HTTP methods used correctly
- [ ] Status codes are appropriate
- [ ] Request/response schemas defined (Zod)
- [ ] Error responses consistent
- [ ] Pagination implemented for lists
- [ ] Authentication required where needed
- [ ] Rate limiting in place
- [ ] Input validation on all endpoints
- [ ] No sensitive data in URLs
- [ ] Versioning strategy defined
- [ ] OpenAPI/Swagger docs generated
API Documentation Template
# openapi.yaml
openapi: 3.0.0
info:
title: Product API
version: 1.0.0
paths:
/products:
get:
summary: List products
parameters:
- name: page
in: query
schema:
type: integer
default: 1
responses:
200:
description: Success
content:
application/json:
schema:
$ref: '#/components/schemas/ProductList'
微信扫一扫