Back to Blog
APIError HandlingRESTBackend

API Error Handling: Building Robust Responses

Design consistent API error responses. Learn error formats, status codes, and client-friendly messages.

B
Bootspring Team
Engineering
February 27, 2026
3 min read

Good error handling makes APIs easier to debug and integrate with.

Consistent Error Format#

1interface ApiError { 2 code: string; // Machine-readable code 3 message: string; // Human-readable message 4 details?: unknown; // Additional context 5 requestId?: string; // For support/debugging 6} 7 8// Example response 9{ 10 "error": { 11 "code": "VALIDATION_ERROR", 12 "message": "Invalid request parameters", 13 "details": { 14 "email": "Must be a valid email address", 15 "age": "Must be at least 18" 16 }, 17 "requestId": "req_abc123" 18 } 19}

HTTP Status Codes#

1// Use appropriate status codes 2const STATUS = { 3 OK: 200, 4 CREATED: 201, 5 NO_CONTENT: 204, 6 BAD_REQUEST: 400, 7 UNAUTHORIZED: 401, 8 FORBIDDEN: 403, 9 NOT_FOUND: 404, 10 CONFLICT: 409, 11 UNPROCESSABLE_ENTITY: 422, 12 TOO_MANY_REQUESTS: 429, 13 INTERNAL_ERROR: 500, 14 SERVICE_UNAVAILABLE: 503, 15};

Custom Error Classes#

1class AppError extends Error { 2 constructor( 3 public code: string, 4 message: string, 5 public statusCode: number = 500, 6 public details?: unknown 7 ) { 8 super(message); 9 this.name = 'AppError'; 10 } 11} 12 13class ValidationError extends AppError { 14 constructor(details: Record<string, string>) { 15 super('VALIDATION_ERROR', 'Invalid request parameters', 422, details); 16 } 17} 18 19class NotFoundError extends AppError { 20 constructor(resource: string) { 21 super('NOT_FOUND', `${resource} not found`, 404); 22 } 23} 24 25class UnauthorizedError extends AppError { 26 constructor(message = 'Authentication required') { 27 super('UNAUTHORIZED', message, 401); 28 } 29}

Error Handling Middleware#

1// Express error handler 2function errorHandler( 3 err: Error, 4 req: Request, 5 res: Response, 6 next: NextFunction 7) { 8 const requestId = req.headers['x-request-id'] || generateId(); 9 10 // Log full error internally 11 logger.error({ 12 requestId, 13 error: err.message, 14 stack: err.stack, 15 path: req.path, 16 method: req.method, 17 }); 18 19 // Handle known errors 20 if (err instanceof AppError) { 21 return res.status(err.statusCode).json({ 22 error: { 23 code: err.code, 24 message: err.message, 25 details: err.details, 26 requestId, 27 }, 28 }); 29 } 30 31 // Handle unknown errors (don't expose internals) 32 res.status(500).json({ 33 error: { 34 code: 'INTERNAL_ERROR', 35 message: 'An unexpected error occurred', 36 requestId, 37 }, 38 }); 39}

Validation Errors#

1import { z } from 'zod'; 2 3const userSchema = z.object({ 4 email: z.string().email(), 5 age: z.number().min(18), 6 name: z.string().min(2), 7}); 8 9function validateRequest(schema: z.ZodSchema) { 10 return (req: Request, res: Response, next: NextFunction) => { 11 const result = schema.safeParse(req.body); 12 13 if (!result.success) { 14 const details = result.error.flatten().fieldErrors; 15 throw new ValidationError( 16 Object.fromEntries( 17 Object.entries(details).map(([key, errors]) => [ 18 key, 19 errors?.join(', ') ?? 'Invalid value', 20 ]) 21 ) 22 ); 23 } 24 25 req.body = result.data; 26 next(); 27 }; 28}

Rate Limiting Errors#

1class RateLimitError extends AppError { 2 constructor(retryAfter: number) { 3 super( 4 'RATE_LIMIT_EXCEEDED', 5 'Too many requests. Please try again later.', 6 429, 7 { retryAfter } 8 ); 9 } 10} 11 12// Include retry-after header 13if (err instanceof RateLimitError) { 14 res.setHeader('Retry-After', err.details.retryAfter); 15}

Async Error Wrapper#

1// Wrap async handlers to catch errors 2const asyncHandler = (fn: RequestHandler): RequestHandler => { 3 return (req, res, next) => { 4 Promise.resolve(fn(req, res, next)).catch(next); 5 }; 6}; 7 8// Usage 9app.get('/users/:id', asyncHandler(async (req, res) => { 10 const user = await db.users.findUnique({ where: { id: req.params.id } }); 11 12 if (!user) { 13 throw new NotFoundError('User'); 14 } 15 16 res.json(user); 17}));

Client-Side Error Handling#

1async function fetchApi<T>(url: string): Promise<T> { 2 const response = await fetch(url); 3 4 if (!response.ok) { 5 const error = await response.json(); 6 7 switch (error.error.code) { 8 case 'UNAUTHORIZED': 9 // Redirect to login 10 window.location.href = '/login'; 11 break; 12 case 'VALIDATION_ERROR': 13 // Show field errors 14 throw new ValidationError(error.error.details); 15 case 'RATE_LIMIT_EXCEEDED': 16 // Show retry message 17 const retryAfter = error.error.details.retryAfter; 18 throw new Error(`Please wait ${retryAfter} seconds`); 19 default: 20 throw new Error(error.error.message); 21 } 22 } 23 24 return response.json(); 25}

Use consistent error formats, appropriate status codes, and never expose internal details to clients.

Share this article

Help spread the word about Bootspring