| name | api-design |
| description | Design and review REST/GraphQL APIs with best practices. Use when creating endpoints, reviewing API architecture, designing data contracts, route handlers, Server Actions, webhooks, or when user mentions "API", "endpoint", "REST", "GraphQL", "route handler", "request/response", or "HTTP methods". |
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:
GET /products?category=electronics&sort=-price&page=2&limit=20
POST /products
{
"name": "Widget",
"price": 29.99,
"categoryId": "cat_123"
}
PATCH /products/123
{
"price": 24.99
}
Response Structure (Consistent Envelope):
{
"data": { },
"meta": {
"pagination": { "page": 1, "limit": 20, "total": 100 }
}
}
{
"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
GET /products?status=active&minPrice=10&maxPrice=100
GET /products?category[in]=electronics,books
GET /products?createdAt[gte]=2024-01-01
GET /products?sort=price
GET /products?sort=-price
GET /products?sort=-createdAt,name
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:
8. Error Handling
Consistent Error Schema:
interface ApiError {
error: {
code: string
message: string
details?: ErrorDetail[]
requestId?: string
}
}
interface ErrorDetail {
field: string
message: string
code?: string
}
Implementation:
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
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.(
(error),
{ : }
)
}
.(
{ : { : , : } },
{ : }
)
}
}
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):
'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') }
})
after(async () => {
await sendNotification(product.id)
})
revalidatePath('/products')
return { success: true, data: product }
}
API Review Checklist
API Documentation Template
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'