codediag

CodeDiag / Guides / Next.js

Is your Next.js middleware protecting your API routes?

Updated November 2, 2026 · Next.js 13–16, App and Pages Router

Only for the paths its matcher covers and the branches that reject. Three gaps are common: a matcher that skips /api, a session check inside a path branch for another section, and auth middleware that loads the session but never blocks.

Next.js's own authentication guide says middleware (called proxy since Next.js 16) shouldn't be your only line of defense. Verify the session in each route handler.

Three ways middleware misses API routes

1. The matcher skips /api

Without a matcher, middleware runs on every request. Many projects start from a matcher that excludes API routes on purpose, then add a session check to it:

middleware.ts
export function middleware(request: NextRequest) {
  if (!request.cookies.get("session")) {
    return NextResponse.redirect(new URL("/login", request.url));
  }
  return NextResponse.next();
}

export const config = {
  matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};

Every page now redirects anonymous visitors. No route handler under /api ever reaches this function.

2. The check sits in another path's branch

middleware.ts
if (!session && request.nextUrl.pathname.startsWith("/dashboard")) {
  return NextResponse.redirect(new URL("/login", request.url));
}

The file "does auth", so it is easy to assume it covers everything. It covers /dashboard.

3. The middleware reads auth but never enforces it

export default clerkMiddleware() makes the auth state available to your routes, but doesn't reject a request until you call auth.protect() or check auth yourself. Clerk's docs now recommend protecting access as close to the resource as possible. The same goes for any middleware that only refreshes a session cookie.

Why middleware shouldn't be the only check

Next.js's authentication guide calls middleware an optimistic check that shouldn't be your only line of defense, and says to verify authentication as close as possible to your data. It also says to treat Server Actions like public API endpoints.

There is a concrete reason. In 2025, CVE-2025-29927 let a request header make self-hosted Next.js skip middleware entirely. It was fixed in 15.2.3, 14.2.25, 13.5.9, and 12.3.5. Apps whose only auth check lived in middleware had every route behind it open.

Fix it: check in the handler

Keep middleware for redirects and a fast first check, and verify the session where the data is touched:

app/api/projects/[id]/route.ts
export async function DELETE(
  _request: Request,
  { params }: { params: Promise<{ id: string }> },
) {
  const session = await auth();
  if (!session?.user) {
    return new Response("Unauthorized", { status: 401 });
  }
  const { id } = await params;
  await db.project.delete({ where: { id, ownerId: session.user.id } });
  return new Response(null, { status: 204 });
}

In the Pages Router, do the same at the top of the API route with your auth library's server helper, such as getServerSession(req, res, authOptions).

Check every route at once

CodeDiag evaluates your middleware (or proxy.ts) the way it runs, for an anonymous request to each route. It follows the matcher, path branches, 401s and redirects, an Auth.js authorized callback, and Supabase's updateSession(). Then it reports the handlers left open. For a route behind the first middleware above:

$ npx @sabahattink/codediag scan .

  ⚠ DELETE /api/projects/:id has no recognizable auth check [missing-auth-check]
    app/api/projects/[id]/route.ts:3
    → Require authentication in the handler or matching middleware

The handler fix above clears it. Routes that verify a webhook signature, such as Stripe constructEvent(), count as authenticated. CodeDiag runs locally, needs no account, and stays offline with --no-audit. It is static analysis, so read the cited file before calling a route vulnerable.

Questions

Does Next.js middleware run for API routes?

Only for paths its matcher covers. Without a matcher it runs on every request; a matcher that excludes api skips every route handler under /api.

What changed in Next.js 16?

The middleware file convention was renamed to proxy (proxy.ts). Matchers work the same way, and the advice to verify auth close to the data still applies. CodeDiag reads both files.

Does clerkMiddleware() protect my routes?

Not on its own. It doesn't reject a request until you call auth.protect() or check auth yourself, and Clerk recommends protecting access close to the resource.

What about Server Actions?

Next.js's authentication guide says to treat them like public API endpoints and verify authentication and authorization inside each one.

Sources

Find every route anyone can call

One command for Next.js, SvelteKit, React Router, Express, Fastify, Hono, Elysia, NestJS, and tRPC. Your coding agent can run it too, through the MCP server or the Claude Code plugin.

$ npx @sabahattink/codediag scan .

Related guides