DEV Community

Cover image for Next.js API Routes: Patterns That Scale
Saad Mehmood
Saad Mehmood

Posted on

Next.js API Routes: Patterns That Scale

Next.js API routes are quick to add but easy to outgrow. Here are patterns I use to keep them maintainable as the app grows.

1. Centralized Error Handling

Don’t repeat try/catch and status codes in every route. Use a small wrapper or middleware:

// lib/api-handler.ts (concept)
export function withErrorHandling(
  handler: (req: NextRequest) => Promise<Response>
) {
  return async (req: NextRequest) => {
    try {
      return await handler(req);
    } catch (error) {
      console.error(error);
      return NextResponse.json(
        { error: 'Internal Server Error' },
        { status: 500 }
      );
    }
  };
}
Enter fullscreen mode Exit fullscreen mode

Use it so each route focuses on success logic; errors are handled in one place.

2. Validate Input

Validate query and body with Zod (or Yup). Return 400 with clear messages instead of blowing up later:

const schema = z.object({
  page: z.coerce.number().min(1).default(1),
  perPage: z.coerce.number().min(1).max(100).default(15),
});

const parsed = schema.safeParse(Object.fromEntries(req.nextUrl.searchParams));
if (!parsed.success) {
  return NextResponse.json(
    { error: parsed.error.flatten() },
    { status: 400 }
  );
}
const { page, perPage } = parsed.data;
Enter fullscreen mode Exit fullscreen mode

Same idea for POST/PUT body: parse JSON, then validate with Zod.

3. Use the Right HTTP Methods and Status Codes

  • GET — read; idempotent. Use 200 (with body) or 204 (no body).
  • POST — create. Return 201 and Location header when you create a resource.
  • PUT/PATCH — update. 200 with body or 204.
  • DELETE — 204 on success.

Return 400 for bad input, 401 for unauthenticated, 403 for forbidden, 404 when the resource doesn’t exist, 429 when rate-limited.

4. Cache Where It Makes Sense

For public or semi-static data, use Next.js caching:

export const revalidate = 60; // ISR: revalidate every 60 seconds
// or
export const dynamic = 'force-static';
Enter fullscreen mode Exit fullscreen mode

For user-specific or private data, avoid caching or use short revalidation and proper auth checks.

5. Move Logic Out of the Route File

Keep route.ts thin: parse request, validate, call a service, return response. Put business logic in lib/ or services/ so you can test and reuse it. Same for DB access: use a repo or client in lib/, not raw queries in the route.

6. Auth and Rate Limiting

  • Check auth (e.g. session or JWT) in middleware or at the start of the handler. Return 401/403 early.
  • Add rate limiting (e.g. Upstash Redis or a simple in-memory store) on public or sensitive endpoints so one client can’t overwhelm the API.

These patterns keep API routes readable and consistent as you add more endpoints and team members.


Saad Mehmood — Portfolio

Top comments (0)