When I audit Next.js apps built quickly (often with AI tools), one pattern shows up more than any other: routes are protected in middleware.ts, and the API route handlers trust that.

// app/api/admin/users/route.ts export async function GET(request: NextRequest) { // "middleware already checked" const users = await db.user.findMany() return NextResponse.json(users) } 

Middleware can be skipped by a matcher that doesn't cover a new route, misconfigured during a refactor, or bypassed by calling the API directly. If the handler doesn't check, the data is open.

The fix is boring: every handler that touches user data verifies the session and role itself.

export async function GET(request: NextRequest) { const session = await getServerSession(authOptions) if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) if (session.user.role !== 'admin') return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) const users = await db.user.findMany() return NextResponse.json(users) } 

Two related checks worth doing while you're there:

  • Hiding the admin dashboard in React when role !== 'admin' is UI gating, not access control. The API behind it still needs the server-side check.
  • Delete a user's session row and make a request with their old cookie. If it still works, revocation isn't wired up.

Rule of thumb: middleware is a convenience layer for redirects and early exits. The security boundary is the handler.

How do you handle this in your apps: a shared requireUser() helper, or checks inline in each route?