Building robust applications requires diligent error handling and input validation. In Next.js API Routes, this is no different. We need to ensure that our API endpoints gracefully handle unexpected situations, provide informative feedback to the client, and reject invalid requests before they can cause problems. This section will guide you through common strategies for achieving this.
The first line of defense in error handling is using appropriate HTTP status codes. These codes communicate the general nature of the response to the client. Here are some common ones you'll use:
400: Bad Request - The request was invalid or could not be understood.
401: Unauthorized - Authentication is required and has failed or not yet been provided.
403: Forbidden - The authenticated user does not have permission to access the resource.
404: Not Found - The requested resource could not be found.
500: Internal Server Error - A generic error message, given when an unexpected condition was encountered and no more specific message is suitable.
503: Service Unavailable - The server is not ready to handle the request.When an error occurs in your API route, you should return a response with the relevant status code and often a JSON payload containing more details about the error.
import type { NextApiRequest, NextApiResponse } from 'next';
export default function handler(req: NextApiRequest, res: NextApiResponse) {
if (!req.body.name) {
return res.status(400).json({ message: 'Name is required' });
}
// ... rest of your logic
res.status(200).json({ message: 'Success' });
}Before processing any request data, it's crucial to validate it. This prevents unexpected behavior and potential security vulnerabilities. Common validation checks include ensuring required fields are present, data types are correct, and values are within acceptable ranges.
For simpler cases, you can perform manual checks directly within your API route.
import type { NextApiRequest, NextApiResponse } from 'next';
interface UserData {
email: string;
age?: number;
}
export default function handler(req: NextApiRequest, res: NextApiResponse) {
const { email, age } = req.body;
if (typeof email !== 'string' || !email.includes('@')) {
return res.status(400).json({ message: 'Invalid email format' });
}
if (age !== undefined && (typeof age !== 'number' || age < 0)) {
return res.status(400).json({ message: 'Age must be a non-negative number' });
}
// ... process validated data
res.status(200).json({ message: 'Data validated successfully' });
}For more complex validation requirements, using a dedicated validation library is highly recommended. Libraries like Zod, Yup, or Joi can significantly simplify your validation logic, provide robust type safety, and offer clear error reporting.
Let's look at an example using Zod for schema-based validation.
// Install zod: npm install zod
import type { NextApiRequest, NextApiResponse } from 'next';
import { z } from 'zod';
const userSchema = z.object({
email: z.string().email('Invalid email address'),
age: z.number().positive('Age must be a positive number').optional(),
});
export default function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const validatedData = userSchema.parse(req.body);
// use validatedData.email and validatedData.age
res.status(200).json({ message: 'Data validated successfully', data: validatedData });
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({
message: 'Validation failed',
errors: error.errors,
});
}
// Handle other potential errors
res.status(500).json({ message: 'Internal Server Error' });
}
}Beyond explicit validation, your API route might encounter unexpected errors during execution, such as database connection issues or unhandled exceptions. A try...catch block is essential for gracefully handling these scenarios.
import type { NextApiRequest, NextApiResponse } from 'next';
export default function handler(req: NextApiRequest, res: NextApiResponse) {
try {
// Your core API logic here
const result = performSomeOperation(req.body);
res.status(200).json(result);
} catch (error) {
console.error('API Error:', error);
// In a real application, you might want to log this error to a service
res.status(500).json({
message: 'An unexpected error occurred. Please try again later.',
});
}
}
function performSomeOperation(data: any) {
// Simulate an operation that might throw an error
if (Math.random() > 0.8) {
throw new Error('Simulated operation failure');
}
return { status: 'Operation successful', data };
}By wrapping your API logic in a try...catch block, you can intercept any uncaught errors, log them for debugging, and return a generic 500 Internal Server Error response to the client, preventing your API from crashing and providing a more user-friendly experience.
graph TD;
A[Client Request] --> B{API Route Handler};
B --> C{Input Validation?};
C -- Yes --> D{Validation Passed?};
D -- Yes --> E[Core API Logic];
D -- No --> F[Return 4xx Error (e.g., 400)];
C -- No --> E;
E --> G{Operation Successful?};
G -- Yes --> H[Return 2xx Success];
G -- No --> I[Handle Internal Error];
I --> J[Log Error];
I --> K[Return 5xx Error (e.g., 500)];
F --> L[Client Receives Error];
H --> L;
K --> L;
When returning error responses, consider the following:
- Be consistent: Establish a standard error response format that you use across all your API routes. This makes it easier for clients to parse and handle errors.
- Avoid sensitive information: Do not expose internal details like stack traces or database error messages in production environments. This could be a security risk.
- Provide actionable information: If possible, include information that helps the client understand what went wrong and how to fix it (e.g., which field failed validation).
- Use descriptive error messages: While avoiding sensitive details, make the error messages clear and understandable.