All writeups
Full Stack

Role-Based Access Control with JWT in Route Handlers

A small guard function that keeps authorization in one place instead of scattered across every endpoint.

May 18, 2026 · 7 min read

Authentication answers who the user is; authorization answers what they may do. Most access control bugs come from the second being checked inconsistently, with one endpoint forgetting a check the others have.

One guard, used everywhere

Instead of repeating role checks in each handler, a single helper verifies the token and the role, and returns either the user or a ready-made error response.

ts
type Role = "admin" | "seller" | "customer";

export async function requireRole(req: Request, ...roles: Role[]) {
  const token = req.headers.get("authorization")?.replace("Bearer ", "");
  if (!token) {
    return Response.json({ error: "Unauthorized" }, { status: 401 });
  }

  const user = await verifyToken(token);
  if (!user) {
    return Response.json({ error: "Unauthorized" }, { status: 401 });
  }

  if (!roles.includes(user.role)) {
    return Response.json({ error: "Forbidden" }, { status: 403 });
  }

  return user;
}

Using it in a handler

ts
export async function DELETE(req: Request) {
  const user = await requireRole(req, "admin");
  if (user instanceof Response) return user;

  // only admins reach this point
}

Things to get right

  • Verify the signature and expiry on the server on every request; never trust a decoded token alone.
  • Check ownership as well as role: a seller should only edit their own products.
  • Keep tokens short-lived and store them in httpOnly cookies when the client is a browser.
Next.jsTypeScriptJWTAuth