typescript 75 lines · 3 tabs

Next.js Route Handler with auth guard

Shared by codesnips Jan 2026
3 tabs
import { cookies } from "next/headers";
import { jwtVerify } from "jose";

export interface Session {
  userId: string;
  role: "user" | "admin";
}

const secret = new TextEncoder().encode(process.env.AUTH_SECRET);

export async function getSession(): Promise<Session | null> {
  const store = await cookies();
  const token = store.get("session")?.value;
  if (!token) return null;

  try {
    const { payload } = await jwtVerify(token, secret);
    if (typeof payload.sub !== "string") return null;
    return {
      userId: payload.sub,
      role: payload.role === "admin" ? "admin" : "user",
    };
  } catch {
    return null;
  }
}
3 files · typescript Explain with highlit

This snippet shows how to secure Next.js App Router Route Handlers with a reusable auth guard instead of repeating session checks inside every endpoint. The pattern centers on a higher-order function that wraps a handler and injects an authenticated context, so each handler can assume a valid user and focus purely on its business logic.

In lib/auth.ts, getSession reads the session cookie, verifies the JWT with jose, and returns a typed Session or null. Verification is done server-side with a secret held in an environment variable, and any failure — expired token, bad signature, missing cookie — collapses to null rather than throwing, so callers never have to wrap it in a try/catch. The Session interface carries the userId and a role, which is enough for both authentication and coarse role-based authorization.

lib/with-auth.ts contains the actual guard. withAuth takes an AuthedHandler and returns a normal Route Handler signature. It calls getSession; if there is none it returns a 401 with a JSON body, short-circuiting before the wrapped handler ever runs. An optional requiredRole enables role checks — when the session role does not match, it responds 403. On success it invokes the handler with the original Request, the route context (so dynamic params still work), and an extra session argument. Because the guard owns the error responses, every protected endpoint returns consistent, machine-readable errors.

app/api/projects/[id]/route.ts demonstrates real usage. The exported GET and DELETE are just withAuth(...) calls; the DELETE also passes "admin" to require elevation. Inside, the handlers destructure session.userId and context.params.id with full type safety and no defensive boilerplate.

The main trade-off is that the guard runs per request in the handler rather than in middleware.ts, which keeps Node APIs and typed sessions available at the cost of not blocking at the edge. A common pitfall is forgetting that params in the App Router may be a promise in newer versions, so the context type should match the installed Next.js version. This composition approach scales well because new authorization rules live in one place.


Related snips

Share this code

Here's the card — post it anywhere.

Next.js Route Handler with auth guard — share card
Link copied