API Development
Systematic REST API development with error handling, validation, and consistent response formats.
Overview
This Skill enforces:
- HTTP status codes (appropriate, not overused)
- RFC 7807 Problem Details for errors
- Input validation and sanitization
- Consistent response formatting
- Request correlation IDs
- Rate limiting
- Security-first error messages
- Centralized error handling
Apply when building API routes, handling errors, or designing responses.
HTTP Status Codes
Status Code Categories
| Range | Purpose | Common Examples |
|---|
| 200-299 | Success | 200 OK, 201 Created, 204 No Content |
| 300-399 | Redirection | 301 Moved Permanently, 302 Found |
| 400-499 | Client Errors | 400 Bad Request, 401 Unauthorized, 404 Not Found |
| 500-599 | Server Errors | 500 Internal Error, 503 Service Unavailable |
Correct Status Codes
ts
1// ✅ GOOD: Specific status codes
2200 // GET: Resource retrieved
3201 // POST: Resource created
4204 // DELETE: Resource deleted (no content)
5400 // Bad Request: Validation failed
6401 // Unauthorized: Not authenticated
7403 // Forbidden: Authenticated but no permission
8404 // Not Found: Resource doesn't exist
9409 // Conflict: Duplicate email
10422 // Unprocessable Entity: Semantic error
11429 // Too Many Requests: Rate limited
12500 // Internal Server Error: Server bug
13
14// ❌ BAD: Vague status codes
15200 // Success response for everything
16500 // Error response for everything
17200 // Returned even when validation failed
Problem Details Structure
ts
1// RFC 7807 Problem Details
2type ProblemDetails = {
3 type: string; // URL to error type documentation
4 title: string; // Short error title
5 status: number; // HTTP status code
6 detail: string; // Specific error details
7 instance?: string; // Request ID for tracking
8 errors?: Record<string, string[]>; // Field-level errors
9};
Implementation
ts
1// lib/errors.ts
2export class ApiError extends Error {
3 constructor(
4 public status: number,
5 public title: string,
6 public detail: string,
7 public type: string = 'about:blank',
8 public errors?: Record<string, string[]>
9 ) {
10 super(detail);
11 this.name = 'ApiError';
12 }
13
14 toJSON() {
15 return {
16 type: this.type,
17 title: this.title,
18 status: this.status,
19 detail: this.detail,
20 instance: this.instance,
21 ...(this.errors && { errors: this.errors })
22 };
23 }
24}
Error Responses
ts
1// ✅ GOOD: RFC 7807 format
2{
3 "type": "https://api.example.com/errors/validation-failed",
4 "title": "Validation Failed",
5 "status": 400,
6 "detail": "The request body contains invalid data",
7 "instance": "req-12345",
8 "errors": {
9 "email": ["Invalid email format"],
10 "age": ["Must be >= 18"]
11 }
12}
13
14// ✅ GOOD: Unauthorized (no sensitive details)
15{
16 "type": "https://api.example.com/errors/unauthorized",
17 "title": "Unauthorized",
18 "status": 401,
19 "detail": "Authentication required",
20 "instance": "req-12346"
21}
22
23// ❌ BAD: Leaks internal details
24{
25 "error": "User not found in database",
26 "stack": "Error: query failed at line 42..."
27}
28
29// ❌ BAD: Not structured
30{
31 "message": "Something went wrong"
32}
Centralized Error Handler
ts
1// middleware/error-handler.ts
2import { NextRequest, NextResponse } from 'next/server';
3import { ApiError } from '@/lib/errors';
4
5export function errorHandler(error: unknown) {
6 const requestId = crypto.randomUUID();
7
8 // Log error (internal, never exposed)
9 console.error(`[${requestId}] Error:`, error);
10
11 // ApiError (predictable)
12 if (error instanceof ApiError) {
13 return NextResponse.json(
14 {
15 type: error.type,
16 title: error.title,
17 status: error.status,
18 detail: error.detail,
19 instance: requestId,
20 ...(error.errors && { errors: error.errors })
21 },
22 { status: error.status }
23 );
24 }
25
26 // Validation error
27 if (error instanceof ZodError) {
28 return NextResponse.json(
29 {
30 type: 'https://api.example.com/errors/validation-failed',
31 title: 'Validation Failed',
32 status: 400,
33 detail: 'The request body contains invalid data',
34 instance: requestId,
35 errors: error.flatten().fieldErrors
36 },
37 { status: 400 }
38 );
39 }
40
41 // Unknown error (generic message)
42 return NextResponse.json(
43 {
44 type: 'https://api.example.com/errors/internal-server-error',
45 title: 'Internal Server Error',
46 status: 500,
47 detail: 'An unexpected error occurred',
48 instance: requestId
49 },
50 { status: 500 }
51 );
52}
Using Error Handler
ts
1// app/api/users/route.ts
2import { errorHandler } from '@/middleware/error-handler';
3
4export async function POST(request: Request) {
5 try {
6 const body = await request.json();
7
8 // Validate
9 const validated = CreateUserSchema.parse(body);
10
11 // Check duplicate
12 const existing = await db.user.findUnique({
13 where: { email: validated.email }
14 });
15
16 if (existing) {
17 throw new ApiError(
18 409,
19 'Conflict',
20 'A user with this email already exists',
21 'https://api.example.com/errors/duplicate-email'
22 );
23 }
24
25 // Create
26 const user = await db.user.create({ data: validated });
27
28 return new Response(JSON.stringify(user), {
29 status: 201,
30 headers: { 'Content-Type': 'application/json' }
31 });
32 } catch (error) {
33 return errorHandler(error);
34 }
35}
Schema Validation
ts
1import { z } from 'zod';
2
3const CreateUserSchema = z.object({
4 email: z.string().email('Invalid email format'),
5 name: z.string().min(1, 'Name required').max(255),
6 age: z.number().int().min(0).max(150),
7 role: z.enum(['admin', 'user', 'guest']).default('user')
8});
9
10// Validate request
11const validated = CreateUserSchema.parse(body);
Sanitization
ts
1import DOMPurify from 'isomorphic-dompurify';
2
3const sanitized = {
4 ...validated,
5 name: DOMPurify.sanitize(validated.name)
6};
Rate Limiting
ts
1import rateLimit from 'express-rate-limit';
2
3// General rate limiter
4const limiter = rateLimit({
5 windowMs: 15 * 60 * 1000, // 15 minutes
6 max: 100, // 100 requests per window
7 message: 'Too many requests, please try again later',
8 standardHeaders: true, // Return rate limit info in headers
9 legacyHeaders: false
10});
11
12// Auth rate limiter (stricter)
13const authLimiter = rateLimit({
14 windowMs: 15 * 60 * 1000,
15 max: 5, // 5 attempts
16 skipSuccessfulRequests: true // Don't count successful logins
17});
18
19app.post('/login', authLimiter, loginHandler);
20app.use('/api/', limiter);
Success Response
ts
1// ✅ GOOD: Consistent response
2export async function GET(request: Request) {
3 const users = await db.user.findMany();
4
5 return NextResponse.json({
6 status: 'success',
7 data: users,
8 meta: {
9 count: users.length,
10 timestamp: new Date().toISOString()
11 }
12 });
13}
14
15// ✅ GOOD: Paginated response
16export async function GET(request: Request) {
17 const page = parseInt(request.nextUrl.searchParams.get('page') || '1');
18 const limit = parseInt(request.nextUrl.searchParams.get('limit') || '20');
19 const offset = (page - 1) * limit;
20
21 const [users, total] = await Promise.all([
22 db.user.findMany({ skip: offset, take: limit }),
23 db.user.count()
24 ]);
25
26 return NextResponse.json({
27 status: 'success',
28 data: users,
29 meta: {
30 pagination: {
31 page,
32 limit,
33 total,
34 pages: Math.ceil(total / limit)
35 }
36 }
37 });
38}
Request Correlation
ts
1// middleware/correlation-id.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export function middleware(request: NextRequest) {
6 const correlationId =
7 request.headers.get('x-correlation-id') ||
8 crypto.randomUUID();
9
10 const response = NextResponse.next();
11 response.headers.set('x-correlation-id', correlationId);
12
13 return response;
14}
15
16// Include in logs
17console.log(`[${correlationId}] User created:`, user);
18
19// Client can track requests
20fetch('/api/users', {
21 headers: { 'x-correlation-id': myRequestId }
22});
Anti-Patterns
ts
1// ❌ BAD: Leaking stack traces
2{
3 "error": "Cannot read property 'id' of undefined at getUserData (line 42)",
4 "stack": "Error: ...\nat app.js:42..."
5}
6
7// ❌ BAD: Generic error message
8{
9 "error": "Something went wrong"
10}
11
12// ❌ BAD: No rate limiting
13// Anyone can hammer API endpoint
14
15// ❌ BAD: Overusing 500
16// Always return 500 for any error
17
18// ❌ BAD: No validation
19const user = await db.user.create(request.body);
20// Raw user input!
Verification Before Production
Integration with Project Standards
Enforces security and usability:
- S-1: No sensitive data in errors
- C-10: Input validated
- AP-8: Validation on server side
Resources
Last Updated: January 24, 2026
Compatibility: Claude Opus 4.5, Claude Code v2.x
Status: Production Ready
January 2026 Update: This skill is compatible with Claude Opus 4.5 and Claude Code v2.x. For complex tasks, use the effort: high parameter for thorough analysis.