Expert guide for RESTful API design, Next.js API routes, error handling, validation, and best practices. Use when building endpoints, handling requests, or designing API architecture.
API Design & Integration Skill
Comprehensive guide for designing and implementing robust, scalable APIs in Next.js applications. From RESTful endpoint design to request validation, error handling, authentication, and rate limiting, this skill covers all aspects of production-ready API development.
Build APIs that are intuitive to use, well-documented, and follow industry best practices. Implement proper HTTP semantics, consistent error responses, and type-safe request/response handling.
Core Workflows
Workflow 1: RESTful Route Handlers
Purpose: Create well-organized API routes following REST conventions
Steps:
- Plan resource-based URL structure
- Create route handlers for each HTTP method
- Implement request validation
- Add proper error handling
- Return consistent response format
Implementation:
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server'
export async function GET(request: NextRequest) {
try {
const users = await db.users.findMany()
return NextResponse.json({ users })
} catch (error) {
return NextResponse.json(
{ error: 'Failed to fetch users' },
{ status: 500 }
)
}
}
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const user = await db.users.create({ data: body })
return NextResponse.json({ user }, { status: 201 })
} catch (error) {
return NextResponse.json(
{ error: 'Failed to create user' },
{ status: 400 }
)
}
}
// app/api/users/[id]/route.ts
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const user = await db.users.findUnique({ where: { id: params.id } })
if (!user) {
return NextResponse.json(
{ error: 'User not found' },
{ status: 404 }
)
}
return NextResponse.json({ user })
}
export async function PATCH(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const body = await request.json()
const user = await db.users.update({
where: { id: params.id },
data: body
})
return NextResponse.json({ user })
}
export async function DELETE(
request: NextRequest,
{ params }: { params: { id: string } }
) {
await db.users.delete({ where: { id: params.id } })
return new NextResponse(null, { status: 204 })
}
Workflow 2: Request Validation with Zod
Purpose: Type-safe request validation with detailed error messages
Implementation:
import { z } from 'zod'
import { NextRequest, NextResponse } from 'next/server'
const userSchema = z.object({
name: z.string().min(2).max(100),
email: z.string().email(),
age: z.number().int().min(18).optional()
})
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const validated = userSchema.parse(body)
const user = await db.users.create({ data: validated })
return NextResponse.json({ user }, { status: 201 })
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{ error: 'Validation failed', issues: error.issues },
{ status: 400 }
)
}
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}
Workflow 3: Authentication Middleware
Purpose: Secure API routes with authentication checks
Implementation:
// lib/auth-middleware.ts
import { NextRequest, NextResponse } from 'next/server'
export async function withAuth(
handler: (req: NextRequest, context: any) => Promise<NextResponse>
) {
return async (req: NextRequest, context: any) => {
const token = req.headers.get('authorization')?.replace('Bearer ', '')
if (!token) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
)
}
try {
const user = await verifyToken(token)
req.user = user
return handler(req, context)
} catch (error) {
return NextResponse.json(
{ error: 'Invalid token' },
{ status: 401 }
)
}
}
}
// app/api/protected/route.ts
export const GET = withAuth(async (request: NextRequest) => {
const user = request.user
return NextResponse.json({ user })
})
Workflow 4: Rate Limiting
Purpose: Protect API from abuse
Implementation:
// lib/rate-limit.ts
const rateLimitMap = new Map<string, { count: number; reset: number }>()
export function rateLimit(limit = 100, window = 60000) {
return async (request: NextRequest) => {
const ip = request.ip || 'anonymous'
const now = Date.now()
const record = rateLimitMap.get(ip)
if (!record || now > record.reset) {
rateLimitMap.set(ip, { count: 1, reset: now + window })
return null
}
if (record.count >= limit) {
return NextResponse.json(
{ error: 'Too many requests' },
{ status: 429 }
)
}
record.count++
return null
}
}
// Usage in route
export async function GET(request: NextRequest) {
const limitResponse = await rateLimit(100, 60000)(request)
if (limitResponse) return limitResponse
// Handle request...
}
Workflow 5: Pagination Pattern
Purpose: Handle large datasets efficiently
Implementation:
type PaginatedResponse<T> = {
data: T[]
pagination: {
page: number
limit: number
total: number
totalPages: number
hasNext: boolean
hasPrev: boolean
}
}
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams
const page = parseInt(searchParams.get('page') || '1')
const limit = parseInt(searchParams.get('limit') || '10')
const search = searchParams.get('search') || ''
const skip = (page - 1) * limit
const [users, total] = await Promise.all([
db.users.findMany({
where: { name: { contains: search } },
skip,
take: limit
}),
db.users.count({ where: { name: { contains: search } } })
])
const totalPages = Math.ceil(total / limit)
return NextResponse.json({
data: users,
pagination: {
page,
limit,
total,
totalPages,
hasNext: page < totalPages,
hasPrev: page > 1
}
})
}
Quick Reference
| Action | HTTP Method | URL Pattern |
|---|---|---|
| List resources | GET | /api/resources |
| Get single | GET | /api/resources/:id |
| Create | POST | /api/resources |
| Update | PATCH/PUT | /api/resources/:id |
| Delete | DELETE | /api/resources/:id |
| Actions | POST | /api/resources/:id/action |
HTTP Status Codes
Success Codes
200 OK- Successful GET, PUT, PATCH201 Created- Successful POST (resource created)204 No Content- Successful DELETE
Client Error Codes
400 Bad Request- Invalid request body/params401 Unauthorized- Missing/invalid authentication403 Forbidden- Authenticated but not authorized404 Not Found- Resource doesn't exist409 Conflict- Resource conflict (duplicate)422 Unprocessable Entity- Validation failed429 Too Many Requests- Rate limit exceeded
Server Error Codes
500 Internal Server Error- Unexpected error503 Service Unavailable- Temporary unavailability
Error Response Format
type ErrorResponse = {
error: string // Human-readable message
code?: string // Machine-readable code
details?: Record<string, any> // Additional context
timestamp?: string
}
// Usage
return NextResponse.json(
{
error: 'Validation failed',
code: 'VALIDATION_ERROR',
details: { field: 'email', reason: 'Invalid format' },
timestamp: new Date().toISOString()
},
{ status: 400 }
)
CORS Configuration
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const response = NextResponse.next()
response.headers.set('Access-Control-Allow-Origin', '*')
response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization')
if (request.method === 'OPTIONS') {
return new NextResponse(null, { status: 200, headers: response.headers })
}
return response
}
export const config = {
matcher: '/api/:path*'
}
Best Practices
- Use HTTP Semantics: Correct status codes (201 for create, 204 for delete)
- Consistent Response Format: Always return
{ data }or{ error, code } - Validate Everything: Use Zod for request body, query, and params
- Handle Errors Gracefully: Never expose internal errors to clients
- Document APIs: Use OpenAPI/Swagger for documentation
- Version APIs: Use /api/v1/ for breaking changes
- Paginate Lists: Always paginate list endpoints
- Use CORS: Configure properly for client-side requests
- Log Requests: Log all API requests for debugging
- Rate Limit: Protect against abuse
Common Patterns
Health Check Endpoint
// app/api/health/route.ts
export async function GET() {
return NextResponse.json({
status: 'ok',
timestamp: new Date().toISOString(),
version: process.env.APP_VERSION
})
}
File Upload
export async function POST(request: NextRequest) {
const formData = await request.formData()
const file = formData.get('file') as File
if (!file) {
return NextResponse.json(
{ error: 'No file provided' },
{ status: 400 }
)
}
const allowedTypes = ['image/jpeg', 'image/png', 'image/webp']
if (!allowedTypes.includes(file.type)) {
return NextResponse.json(
{ error: 'Invalid file type' },
{ status: 400 }
)
}
const url = await uploadFile(file)
return NextResponse.json({ url }, { status: 201 })
}
Webhook Handler
export async function POST(request: NextRequest) {
const signature = request.headers.get('x-webhook-signature')
const body = await request.text()
const isValid = verifyWebhookSignature(body, signature)
if (!isValid) {
return NextResponse.json(
{ error: 'Invalid signature' },
{ status: 401 }
)
}
const event = JSON.parse(body)
await processWebhookEvent(event)
return NextResponse.json({ received: true })
}
Dependencies
# Validation
npm install zod
# Rate limiting
npm install @upstash/redis @upstash/ratelimit
# API documentation
npm install swagger-ui-react swagger-jsdoc
Error Handling
- 400 Bad Request: Invalid input, validation errors
- 401 Unauthorized: Missing or invalid authentication
- 403 Forbidden: Valid auth but insufficient permissions
- 404 Not Found: Resource doesn't exist
- 409 Conflict: Resource already exists or state conflict
- 422 Unprocessable: Valid syntax but semantic errors
- 429 Too Many Requests: Rate limit exceeded
- 500 Internal Error: Server-side failures
Performance Tips
- Use database indexes for query fields
- Implement cursor-based pagination for large datasets
- Cache frequent reads with Redis or SWR
- Use database connection pooling
- Minimize payload size with field selection
- Compress responses with gzip
When to Use This Skill
Invoke this skill when:
- Creating new API endpoints
- Implementing request validation
- Designing API architecture
- Adding authentication/authorization
- Handling file uploads
- Implementing webhooks
- Setting up CORS
- Adding rate limiting
- Debugging API errors
- Optimizing API performance
You Might Also Like
Related Skills

verify
Use when you want to validate changes before committing, or when you need to check all React contribution requirements.
facebook
test
Use when you need to run tests for React core. Supports source, www, stable, and experimental channels.
facebook
feature-flags
Use when feature flag tests fail, flags need updating, understanding @gate pragmas, debugging channel-specific test failures, or adding new flags to React.
facebook
extract-errors
Use when adding new error messages to React, or seeing "unknown error code" warnings.
facebook