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;
}
}
import { NextResponse } from "next/server";
import { getSession, Session } from "./auth";
type RouteContext = { params: Promise<Record<string, string>> };
export type AuthedHandler = (
req: Request,
ctx: RouteContext,
session: Session,
) => Promise<Response> | Response;
export function withAuth(
handler: AuthedHandler,
requiredRole?: Session["role"],
) {
return async (req: Request, ctx: RouteContext): Promise<Response> => {
const session = await getSession();
if (!session) {
return NextResponse.json({ error: "unauthenticated" }, { status: 401 });
}
if (requiredRole && session.role !== requiredRole) {
return NextResponse.json({ error: "forbidden" }, { status: 403 });
}
return handler(req, ctx, session);
};
}
import { NextResponse } from "next/server";
import { withAuth } from "@/lib/with-auth";
import { getProject, deleteProject } from "@/lib/projects";
export const GET = withAuth(async (_req, ctx, session) => {
const { id } = await ctx.params;
const project = await getProject(id);
if (!project || project.ownerId !== session.userId) {
return NextResponse.json({ error: "not_found" }, { status: 404 });
}
return NextResponse.json(project);
});
export const DELETE = withAuth(async (_req, ctx, session) => {
const { id } = await ctx.params;
await deleteProject(id, session.userId);
return new NextResponse(null, { status: 204 });
}, "admin");
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
payload = {
sub: user.id,
iss: 'https://auth.example.com',
aud: 'codesnips-api',
exp: 15.minutes.from_now.to_i,
iat: Time.now.to_i,
JWT issuance and verification without common footguns
export interface RetryOptions {
retries: number;
baseMs: number;
maxMs: number;
signal?: AbortSignal;
onRetry?: (attempt: number, delay: number, err: unknown) => void;
Exponential backoff with jitter for retries
import axios, { AxiosError } from 'axios'
import { v4 as uuidv4 } from 'uuid'
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000/api/v1',
timeout: 15000,
Axios API client with interceptors
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
interface FilterState {
search: string
category: string | null
Zustand for lightweight state management
import { randomBytes, createHash } from "crypto";
import jwt from "jsonwebtoken";
import { RefreshTokenStore } from "./store";
const ACCESS_SECRET = process.env.ACCESS_SECRET!;
const ACCESS_TTL = "15m";
JWT access + refresh token rotation (conceptual)
package deps
import (
"net"
"net/http"
"time"
HTTP client tuned for production: timeouts, transport, and connection reuse
Share this code
Here's the card — post it anywhere.