export const PERMISSIONS = {
READ_POSTS: 'posts:read',
WRITE_POSTS: 'posts:write',
DELETE_POSTS: 'posts:delete',
MANAGE_USERS: 'users:manage',
} as const;
export type Permission = (typeof PERMISSIONS)[keyof typeof PERMISSIONS];
export type Role = 'viewer' | 'editor' | 'admin';
export const ROLE_PERMISSIONS: Record<Role, readonly Permission[]> = {
viewer: [PERMISSIONS.READ_POSTS],
editor: [PERMISSIONS.READ_POSTS, PERMISSIONS.WRITE_POSTS],
admin: [
PERMISSIONS.READ_POSTS,
PERMISSIONS.WRITE_POSTS,
PERMISSIONS.DELETE_POSTS,
PERMISSIONS.MANAGE_USERS,
],
};
export function hasPermission(role: Role, permission: Permission): boolean {
return ROLE_PERMISSIONS[role].includes(permission);
}
import { Request, Response, NextFunction, RequestHandler } from 'express';
import { Permission, Role, hasPermission } from './permissions';
export interface AuthUser {
id: string;
role: Role;
}
declare global {
namespace Express {
interface Request {
user?: AuthUser;
}
}
}
export function requirePermission(permission: Permission): RequestHandler {
return (req: Request, res: Response, next: NextFunction): void => {
const user = req.user;
if (!user) {
res.status(401).json({ error: 'Authentication required' });
return;
}
if (!hasPermission(user.role, permission)) {
res.status(403).json({ error: 'Insufficient permissions' });
return;
}
next();
};
}
export function requireRole(...roles: Role[]): RequestHandler {
return (req: Request, res: Response, next: NextFunction): void => {
if (!req.user) {
res.status(401).json({ error: 'Authentication required' });
return;
}
if (!roles.includes(req.user.role)) {
res.status(403).json({ error: 'Insufficient permissions' });
return;
}
next();
};
}
import { Router, Request, Response } from 'express';
import { authenticate } from './authenticate';
import { requirePermission, requireRole } from './authorize';
import { PERMISSIONS } from './permissions';
import * as posts from './postsController';
const router = Router();
router.use(authenticate);
router.get(
'/posts',
requirePermission(PERMISSIONS.READ_POSTS),
posts.list,
);
router.post(
'/posts',
requirePermission(PERMISSIONS.WRITE_POSTS),
posts.create,
);
router.delete(
'/posts/:id',
requirePermission(PERMISSIONS.DELETE_POSTS),
posts.remove,
);
router.get(
'/admin/users',
requireRole('admin'),
(req: Request, res: Response) => {
res.json({ requestedBy: req.user?.id });
},
);
export default router;
This snippet shows how role-based access control (RBAC) is implemented in an Express API with a type-safe permission layer instead of scattering if (user.role === 'admin') checks across handlers. The core idea is to centralize the mapping between roles and permissions, then express route requirements declaratively as middleware.
In permissions.ts, permissions are modeled as a union of string literals (Permission) derived from a constant object, so the compiler knows the full set at build time. The ROLE_PERMISSIONS record maps each Role to the permissions it grants, and hasPermission performs the lookup. Because everything is typed, a typo like posts:delee is a compile error, and adding a new permission forces the developer to decide which roles receive it. This is the trade-off RBAC makes: a small amount of upfront modeling in exchange for authorization logic that lives in one auditable place.
The requirePermission middleware in authorize.ts turns that model into Express guards. It augments the Request type with a user field so downstream handlers get full typing without casts. requirePermission is a factory: it accepts a Permission and returns a RequestHandler, which is the idiomatic Express pattern for parameterized middleware. It first checks that a user was attached (returning 401 when authentication is missing) and then checks the permission (returning 403 when the user is known but not allowed) — keeping the 401/403 distinction correct matters for clients. requireRole offers a coarser guard for cases where a whole role gates a route.
The router.ts tab wires it together: authenticate runs first to populate req.user from a token, and each route composes the relevant guard before its handler. Because guards are just middleware, they stack naturally and read like a specification of who may do what. A key pitfall to avoid is ordering — the guard must run after authentication, and any error thrown asynchronously should be forwarded via next. This approach scales better than inline role checks and keeps permission changes from rippling through every controller.
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
module Api
module V1
class UsersController < BaseController
def show
user = User.includes(:profile).find(params[:id])
ETags for conditional requests and caching
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
createdAt: String!
GraphQL API with Spring Boot
#!/usr/bin/env bash
set -euo pipefail
export VAULT_ADDR="https://vault.internal:8200"
export VAULT_TOKEN="${VAULT_TOKEN:?missing VAULT_TOKEN}"
Secrets management with environment isolation and Vault
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
Share this code
Here's the card — post it anywhere.