Errors happen. Networks fail, users send invalid data, resources don't exist, and databases go down. Proper error handling makes the difference between a fragile API and a production-ready one. Use the right HTTP status codes, provide helpful error messages, log errors for debugging, and handle edge cases gracefully. Build APIs that fail gracefully and help developers debug issues!
HTTP Status Codes
Status Code Categories
2xx - Success
- 200 OK: Request succeeded (GET, PUT, PATCH)
- 201 Created: Resource created successfully (POST)
- 204 No Content: Success with no response body (DELETE)
4xx - Client Errors
- 400 Bad Request: Invalid request (validation errors)
- 401 Unauthorized: Authentication required
- 403 Forbidden: Authenticated but insufficient permissions
- 404 Not Found: Resource doesn't exist
- 409 Conflict: Request conflicts with current state
- 422 Unprocessable Entity: Validation error (alternative to 400)
- 429 Too Many Requests: Rate limit exceeded
5xx - Server Errors
- 500 Internal Server Error: Unexpected server error
- 502 Bad Gateway: Invalid response from upstream server
- 503 Service Unavailable: Server temporarily unavailable
Choosing the Right Status Code
// ✅ 200 OK - Successful GET, PUT, PATCH
export async function GET() {
const data = await fetchData();
return Response.json(data); // 200 by default
}
// ✅ 201 Created - Successful POST
export async function POST(request: Request) {
const data = await request.json();
const created = await db.create(data);
return Response.json(created, { status: 201 });
}
// ✅ 204 No Content - Successful DELETE (no body)
export async function DELETE() {
await db.delete();
return new Response(null, { status: 204 });
}
// ✅ 400 Bad Request - Invalid input
if (!email || !password) {
return Response.json(
{ error: 'Email and password required' },
{ status: 400 }
);
}
// ✅ 401 Unauthorized - No authentication
if (!authToken) {
return Response.json(
{ error: 'Authentication required' },
{ status: 401 }
);
}
// ✅ 403 Forbidden - Authenticated but no permission
if (user.role !== 'admin') {
return Response.json(
{ error: 'Admin access required' },
{ status: 403 }
);
}
// ✅ 404 Not Found - Resource doesn't exist
if (!resource) {
return Response.json(
{ error: 'Resource not found' },
{ status: 404 }
);
}
// ✅ 409 Conflict - Duplicate resource
if (await emailExists(email)) {
return Response.json(
{ error: 'Email already registered' },
{ status: 409 }
);
}
// ✅ 500 Internal Server Error - Unexpected errors
catch (error) {
console.error('Unexpected error:', error);
return Response.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}Error Response Patterns
Simple Error Response
export async function POST(request: Request) {
const body = await request.json();
// Simple error response
if (!body.title) {
return Response.json(
{ error: 'Title is required' },
{ status: 400 }
);
}
const post = await db.posts.create({ data: body });
return Response.json(post, { status: 201 });
}
// Response for error:
// {
// "error": "Title is required"
// }
// ✅ Simple and clear
// ✅ Single error message
// ✅ Use for simple validationDetailed Error Response
export async function POST(request: Request) {
const body = await request.json();
// Detailed error response
const errors: string[] = [];
if (!body.title) {
errors.push('Title is required');
}
if (body.title && body.title.length < 3) {
errors.push('Title must be at least 3 characters');
}
if (!body.content) {
errors.push('Content is required');
}
if (errors.length > 0) {
return Response.json(
{
error: 'Validation failed',
message: 'Please correct the following errors',
errors,
},
{ status: 400 }
);
}
const post = await db.posts.create({ data: body });
return Response.json(post, { status: 201 });
}
// Response for errors:
// {
// "error": "Validation failed",
// "message": "Please correct the following errors",
// "errors": [
// "Title is required",
// "Content is required"
// ]
// }
// ✅ Multiple errors
// ✅ Clear error message
// ✅ List of specific issuesField-Specific Errors
import { z } from 'zod';
const createUserSchema = z.object({
email: z.string().email('Invalid email format'),
password: z.string().min(8, 'Password must be at least 8 characters'),
age: z.number().min(18, 'Must be at least 18 years old'),
});
export async function POST(request: Request) {
const body = await request.json();
const result = createUserSchema.safeParse(body);
if (!result.success) {
return Response.json(
{
error: 'Validation failed',
fields: result.error.flatten().fieldErrors,
},
{ status: 400 }
);
}
const user = await db.users.create({ data: result.data });
return Response.json(user, { status: 201 });
}
// Response for validation errors:
// {
// "error": "Validation failed",
// "fields": {
// "email": ["Invalid email format"],
// "password": ["Password must be at least 8 characters"],
// "age": ["Must be at least 18 years old"]
// }
// }
// ✅ Field-specific errors
// ✅ Easy for forms to display
// ✅ Zod integrationError with Code and Metadata
export async function POST(request: Request) {
const body = await request.json();
const user = await db.users.findUnique({
where: { email: body.email },
});
if (!user) {
return Response.json(
{
error: {
code: 'INVALID_CREDENTIALS',
message: 'Invalid email or password',
timestamp: new Date().toISOString(),
path: '/api/auth/login',
},
},
{ status: 401 }
);
}
const validPassword = await verifyPassword(
body.password,
user.hashedPassword
);
if (!validPassword) {
return Response.json(
{
error: {
code: 'INVALID_CREDENTIALS',
message: 'Invalid email or password',
timestamp: new Date().toISOString(),
path: '/api/auth/login',
},
},
{ status: 401 }
);
}
const token = await createToken(user.id);
return Response.json({ token });
}
// Response for error:
// {
// "error": {
// "code": "INVALID_CREDENTIALS",
// "message": "Invalid email or password",
// "timestamp": "2024-01-15T10:30:00.000Z",
// "path": "/api/auth/login"
// }
// }
// ✅ Error code for programmatic handling
// ✅ Timestamp for logging
// ✅ Path for context
// ✅ Consistent structureTry-Catch Error Handling
Basic Try-Catch
export async function GET(request: Request) {
try {
const posts = await db.posts.findMany();
return Response.json(posts);
} catch (error) {
console.error('Failed to fetch posts:', error);
return Response.json(
{ error: 'Failed to fetch posts' },
{ status: 500 }
);
}
}
// ✅ Catch unexpected errors
// ✅ Log error details
// ✅ Return generic message
// ✅ Don't expose internal detailsSpecific Error Handling
export async function DELETE(
request: Request,
{ params }: { params: { id: string } }
) {
try {
await db.posts.delete({
where: { id: params.id },
});
return new Response(null, { status: 204 });
} catch (error) {
// Check for specific Prisma errors
if (error.code === 'P2025') {
// Record not found
return Response.json(
{ error: 'Post not found' },
{ status: 404 }
);
}
if (error.code === 'P2003') {
// Foreign key constraint
return Response.json(
{ error: 'Cannot delete post with existing comments' },
{ status: 409 }
);
}
// Generic error
console.error('Delete error:', error);
return Response.json(
{ error: 'Failed to delete post' },
{ status: 500 }
);
}
}
// ✅ Handle specific database errors
// ✅ Appropriate status codes
// ✅ Helpful error messages
// ✅ Fallback to generic errorError Handler Utility
// Custom error classes
export class ValidationError extends Error {
constructor(
message: string,
public fields?: Record<string, string[]>
) {
super(message);
this.name = 'ValidationError';
}
}
export class NotFoundError extends Error {
constructor(message: string = 'Resource not found') {
super(message);
this.name = 'NotFoundError';
}
}
export class UnauthorizedError extends Error {
constructor(message: string = 'Unauthorized') {
super(message);
this.name = 'UnauthorizedError';
}
}
export class ForbiddenError extends Error {
constructor(message: string = 'Forbidden') {
super(message);
this.name = 'ForbiddenError';
}
}
// Error handler
export function handleError(error: unknown): Response {
// Log error
console.error('API Error:', error);
// Validation error
if (error instanceof ValidationError) {
return Response.json(
{
error: error.message,
fields: error.fields,
},
{ status: 400 }
);
}
// Not found error
if (error instanceof NotFoundError) {
return Response.json(
{ error: error.message },
{ status: 404 }
);
}
// Unauthorized error
if (error instanceof UnauthorizedError) {
return Response.json(
{ error: error.message },
{ status: 401 }
);
}
// Forbidden error
if (error instanceof ForbiddenError) {
return Response.json(
{ error: error.message },
{ status: 403 }
);
}
// Generic error
return Response.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
// ✅ Custom error classes
// ✅ Centralized error handling
// ✅ Consistent responses
// ✅ Type-safeUsing Error Handler
import { handleError, NotFoundError, ValidationError } from '@/app/lib/errors';
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
try {
const post = await db.posts.findUnique({
where: { id: params.id },
});
if (!post) {
throw new NotFoundError('Post not found');
}
return Response.json(post);
} catch (error) {
return handleError(error);
}
}
export async function PUT(
request: Request,
{ params }: { params: { id: string } }
) {
try {
const body = await request.json();
// Validate
if (!body.title || body.title.length < 3) {
throw new ValidationError('Validation failed', {
title: ['Title must be at least 3 characters'],
});
}
// Update
const post = await db.posts.update({
where: { id: params.id },
data: body,
});
return Response.json(post);
} catch (error) {
return handleError(error);
}
}
// ✅ Throw custom errors
// ✅ Single error handler
// ✅ Consistent error responses
// ✅ Clean codeValidation Error Patterns
Zod Validation with Detailed Errors
import { z } from 'zod';
const createPostSchema = z.object({
title: z
.string()
.min(3, 'Title must be at least 3 characters')
.max(200, 'Title must not exceed 200 characters'),
content: z
.string()
.min(10, 'Content must be at least 10 characters'),
published: z.boolean().default(false),
tags: z
.array(z.string())
.min(1, 'At least one tag required')
.max(5, 'Maximum 5 tags allowed')
.optional(),
});
export async function POST(request: Request) {
try {
const body = await request.json();
const result = createPostSchema.safeParse(body);
if (!result.success) {
const errors = result.error.flatten();
return Response.json(
{
error: 'Validation failed',
message: 'Please correct the following errors',
fields: errors.fieldErrors,
// Optional: include form errors
formErrors: errors.formErrors,
},
{ status: 400 }
);
}
const post = await db.posts.create({
data: result.data,
});
return Response.json(post, { status: 201 });
} catch (error) {
console.error('Create post error:', error);
return Response.json(
{ error: 'Failed to create post' },
{ status: 500 }
);
}
}
// POST /api/posts
// Body: { "title": "Hi", "content": "Short" }
// Response:
// {
// "error": "Validation failed",
// "message": "Please correct the following errors",
// "fields": {
// "title": ["Title must be at least 3 characters"],
// "content": ["Content must be at least 10 characters"]
// }
// }
// ✅ Zod for validation
// ✅ Field-specific errors
// ✅ Custom error messages
// ✅ Type-safe validationAsync Validation (Database Checks)
import { z } from 'zod';
const createUserSchema = z.object({
email: z.string().email('Invalid email format'),
username: z.string().min(3, 'Username must be at least 3 characters'),
password: z.string().min(8, 'Password must be at least 8 characters'),
});
export async function POST(request: Request) {
try {
const body = await request.json();
// Schema validation
const result = createUserSchema.safeParse(body);
if (!result.success) {
return Response.json(
{
error: 'Validation failed',
fields: result.error.flatten().fieldErrors,
},
{ status: 400 }
);
}
const data = result.data;
// Async validation - check uniqueness
const existingEmail = await db.users.findUnique({
where: { email: data.email },
});
if (existingEmail) {
return Response.json(
{
error: 'Validation failed',
fields: {
email: ['Email already registered'],
},
},
{ status: 409 }
);
}
const existingUsername = await db.users.findUnique({
where: { username: data.username },
});
if (existingUsername) {
return Response.json(
{
error: 'Validation failed',
fields: {
username: ['Username already taken'],
},
},
{ status: 409 }
);
}
// Create user
const user = await db.users.create({
data: {
...data,
hashedPassword: await hashPassword(data.password),
},
});
return Response.json(
{ id: user.id, email: user.email },
{ status: 201 }
);
} catch (error) {
console.error('Create user error:', error);
return Response.json(
{ error: 'Failed to create user' },
{ status: 500 }
);
}
}
// ✅ Schema validation first
// ✅ Then async database checks
// ✅ 409 Conflict for duplicates
// ✅ Consistent error formatError Handling Structure
Organization of error handling and utilities
Select a file or folder to see details
Logging and Monitoring
Structured Logging
export async function POST(request: Request) {
const startTime = Date.now();
try {
const body = await request.json();
// Log request
console.log('POST /api/posts', {
timestamp: new Date().toISOString(),
body,
});
const post = await db.posts.create({ data: body });
// Log success
const duration = Date.now() - startTime;
console.log('POST /api/posts - Success', {
duration: `${duration}ms`,
postId: post.id,
});
return Response.json(post, { status: 201 });
} catch (error) {
// Log error with context
const duration = Date.now() - startTime;
console.error('POST /api/posts - Error', {
duration: `${duration}ms`,
error: error instanceof Error ? error.message : 'Unknown error',
stack: error instanceof Error ? error.stack : undefined,
timestamp: new Date().toISOString(),
});
return Response.json(
{ error: 'Failed to create post' },
{ status: 500 }
);
}
}
// ✅ Structured logs
// ✅ Include context
// ✅ Track duration
// ✅ Timestamp everythingError Monitoring Service
// Example with Sentry (or similar service)
import * as Sentry from '@sentry/nextjs';
export function logError(error: unknown, context?: Record<string, any>) {
// Log to console
console.error('Error:', error, context);
// Send to monitoring service
if (process.env.NODE_ENV === 'production') {
Sentry.captureException(error, {
extra: context,
});
}
}
export function logApiError(
error: unknown,
request: Request,
endpoint: string
) {
const context = {
endpoint,
method: request.method,
url: request.url,
timestamp: new Date().toISOString(),
};
logError(error, context);
}
// ✅ Centralized error logging
// ✅ Send to monitoring service
// ✅ Include request context
// ✅ Production-onlyUsing Monitoring in Routes
import { logApiError } from '@/app/lib/monitoring';
export async function POST(request: Request) {
try {
const body = await request.json();
const post = await db.posts.create({ data: body });
return Response.json(post, { status: 201 });
} catch (error) {
// Log to monitoring service
logApiError(error, request, 'POST /api/posts');
return Response.json(
{ error: 'Failed to create post' },
{ status: 500 }
);
}
}
// ✅ Track all errors
// ✅ Get notified of issues
// ✅ Debug production problems
// ✅ Monitor API healthError Handling Best Practices
1. Always Use Try-Catch for Async Operations
// ✅ GOOD: Try-catch for database calls
export async function GET() {
try {
const data = await db.items.findMany();
return Response.json(data);
} catch (error) {
console.error('Database error:', error);
return Response.json(
{ error: 'Failed to fetch items' },
{ status: 500 }
);
}
}
// ❌ BAD: No error handling
export async function GET() {
const data = await db.items.findMany(); // Crashes on error!
return Response.json(data);
}2. Never Expose Internal Errors
// ✅ GOOD: Generic error message
catch (error) {
console.error('Internal error:', error); // Log details
return Response.json(
{ error: 'Failed to process request' }, // Generic message
{ status: 500 }
);
}
// ❌ BAD: Expose stack trace
catch (error) {
return Response.json(
{ error: error.message, stack: error.stack }, // Reveals internals!
{ status: 500 }
);
}3. Return Helpful Error Messages
// ✅ GOOD: Actionable error messages
if (!email) {
return Response.json(
{ error: 'Email is required. Please provide a valid email address.' },
{ status: 400 }
);
}
// ❌ BAD: Vague error messages
if (!email) {
return Response.json(
{ error: 'Invalid input' }, // What's invalid?
{ status: 400 }
);
}4. Use Consistent Error Format
// ✅ GOOD: Consistent structure
// All errors follow same format
{
error: 'Error message',
code: 'ERROR_CODE', // Optional
fields: { ... }, // Optional for validation
}
// ❌ BAD: Inconsistent formats
// Sometimes { error: '...' }
// Sometimes { message: '...' }
// Sometimes { errors: [...] }
// Pick one format and stick to it!5. Log Errors with Context
// ✅ GOOD: Log with context
catch (error) {
console.error('API Error', {
endpoint: '/api/posts',
method: request.method,
error: error.message,
userId: currentUserId,
timestamp: new Date().toISOString(),
});
return Response.json(
{ error: 'Failed to process request' },
{ status: 500 }
);
}
// Logs help debug production issuesKey Takeaways
- 400 for validation errors - client sent invalid data
- 401 vs 403 - 401 = no auth, 403 = no permission
- 404 for missing resources - resource doesn't exist
- 500 for server errors - unexpected internal errors
- Always use try-catch - handle async errors
- Never expose internals - log details, return generic messages
- Field-specific errors - help users fix issues
- Consistent error format - same structure everywhere
🎉 API Routes Section Complete!
You've completed the API Routes and Route Handlers section! You've mastered:
- ✅ Route Handlers Introduction
- ✅ GET and POST Request Handlers
- ✅ Dynamic API Routes
- ✅ Request and Response Objects
- ✅ API Error Handling and Status Codes
You now have complete mastery of building APIs in Next.js! You can create RESTful endpoints with proper HTTP methods, handle dynamic routes with parameters, work with headers and cookies, implement robust error handling with appropriate status codes, and build production-ready APIs that handle edge cases gracefully. These skills enable you to build complete full-stack applications with Next.js!
🔍 Production Checklist
- ✅ All endpoints have try-catch error handling
- ✅ Appropriate HTTP status codes for all responses
- ✅ Input validation with Zod or similar
- ✅ Authentication and authorization checks
- ✅ Error logging with context
- ✅ No internal details exposed in errors
- ✅ Consistent error response format
- ✅ Rate limiting for public endpoints