Most apps have two kinds of routes: public ones anyone can hit, and protected ones that require authenticationWhat is authentication?Verifying who a user is, typically through credentials like a password or token.. MiddlewareWhat is middleware?A function that runs between receiving a request and sending a response. It can check authentication, log data, or modify the request before your main code sees it. is how you enforce that boundary cleanly, without repeating the same tokenWhat is token?The smallest unit of text an LLM processes - roughly three-quarters of a word. API pricing is based on how many tokens you use.-check logic in every route handlerWhat is route handler?A Next.js file named route.js inside the app/ directory that handles HTTP requests directly - the App Router equivalent of API routes..
Think of middleware like a bouncer at the door. Every request walks up, the bouncer checks their ID, and only the valid ones get waved through. The bouncer doesn't care what the person is going to do inside, that's the route handler's job.
How the request pipelineWhat is pipeline?A sequence of automated steps (install, lint, test, build, deploy) that code passes through before reaching production. works
Express processes a request through a chain of functions before it reaches your route handlerWhat is route handler?A Next.js file named route.js inside the app/ directory that handles HTTP requests directly - the App Router equivalent of API routes.. Each function in the chain receives req, res, and next. Calling next() passes control to the next function; not calling it stops the chain.
Request → requireAuth → requireRole → validate → Route Handler → Response
| | |
Check JWT Check role Validate body
401/403 403 400The order matters. You want authenticationWhat is authentication?Verifying who a user is, typically through credentials like a password or token. checked before authorizationWhat is authorization?Checking what an authenticated user is allowed to do, like whether they can delete records or access admin pages., and authorization before business logic.
Creating auth middlewareWhat is middleware?A function that runs between receiving a request and sending a response. It can check authentication, log data, or modify the request before your main code sees it.
JWTWhat is jwt?JSON Web Token - a self-contained, signed token that carries user data (like user ID and role). The server can verify it without a database lookup. authenticationWhat is authentication?Verifying who a user is, typically through credentials like a password or token. middleware
// middleware/auth.js
import jwt from 'jsonwebtoken';
export function requireAuth(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
if (!token) {
return res.status(401).json({
error: 'Access denied',
message: 'No token provided'
});
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded; // Attach user for downstream use
next();
} catch (error) {
if (error.name === 'TokenExpiredError') {
return res.status(401).json({
error: 'Token expired',
message: 'Please log in again'
});
}
return res.status(403).json({
error: 'Invalid token',
message: 'Token verification failed'
});
}
}SessionWhat is session?A server-side record that tracks a logged-in user. The browser holds only a session ID in a cookie, and the server looks up the full data on each request.-based auth middleware
If you use sessions instead of JWTs, the check is simpler, you just verify the session store has a valid entry:
export function requireSession(req, res, next) {
if (!req.session.userId) {
return res.status(401).json({
error: 'Not authenticated',
message: 'Please log in'
});
}
req.user = {
id: req.session.userId,
role: req.session.role
};
next();
}401 and 403: use 401 when the user isn't authenticated at all (no token, expired token). Use 403 when they are authenticated but don't have permission for the specific resource.Role-based middlewareWhat is middleware?A function that runs between receiving a request and sending a response. It can check authentication, log data, or modify the request before your main code sees it.
Checking roles
Once you know who someone is, you can check what they're allowed to do. A role middleware factory takes the permitted roles as arguments and returns a middleware function:
export function requireRole(...allowedRoles) {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'Not authenticated' });
}
if (!allowedRoles.includes(req.user.role)) {
return res.status(403).json({
error: 'Forbidden',
message: `Role '${req.user.role}' not authorized`
});
}
next();
};
}
// Usage
app.delete('/users/:id',
requireAuth,
requireRole('admin', 'moderator'),
deleteUser
);Fine-grained permission checks
Roles are coarse-grained. For more control, you can check specific permissions stored in the database:
export function requirePermission(permission) {
return async (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'Not authenticated' });
}
const userPermissions = await getUserPermissions(req.user.id);
if (!userPermissions.includes(permission)) {
return res.status(403).json({
error: 'Forbidden',
message: `Missing permission: ${permission}`
});
}
next();
};
}
// Usage
app.post('/posts',
requireAuth,
requirePermission('posts:create'),
createPost
);Resource ownership
Role checks protect categories of users, but sometimes you need to protect individual resources. An invoice should only be visible to the user who created it (and admins):
export function requireOwnership(getResourceOwner) {
return async (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'Not authenticated' });
}
const resourceId = req.params.id;
const ownerId = await getResourceOwner(resourceId);
if (req.user.id !== ownerId && req.user.role !== 'admin') {
return res.status(403).json({
error: 'Forbidden',
message: 'You do not own this resource'
});
}
next();
};
}
// Usage
app.put('/posts/:id',
requireAuth,
requireOwnership(async (postId) => {
const post = await db.getPost(postId);
return post.authorId;
}),
updatePost
);userId sent from the client. An attacker can trivially change that value.Composing middlewareWhat is middleware?A function that runs between receiving a request and sending a response. It can check authentication, log data, or modify the request before your main code sees it. chains
The real power comes from chaining these pieces together. A typical set of routes looks like this:
// Public routes (no middleware)
app.post('/login', login);
app.post('/register', register);
// Protected routes
app.get('/profile', requireAuth, getProfile);
app.put('/profile', requireAuth, validate(updateProfileSchema), updateProfile);
// Admin-only routes
app.get('/admin/users', requireAuth, requireRole('admin'), getAllUsers);
app.delete('/admin/users/:id', requireAuth, requireRole('admin'), deleteUser);
// Ownership-protected routes
app.get('/posts/:id', getPost); // Public read
app.post('/posts', requireAuth, validate(createPostSchema), createPost);
app.put('/posts/:id', requireAuth, requireOwnership(getPostOwner), updatePost);
app.delete('/posts/:id', requireAuth, requireOwnership(getPostOwner), deletePost);The order in each chain is deliberate: check identity first, then check permissions, then validate the request body.
Quick reference
| Status code | Meaning | When to use |
|---|---|---|
401 | Unauthorized | No token, expired token, invalid credentials |
403 | Forbidden | Valid token but insufficient role or ownership |
400 | Bad Request | Validation failed (wrong body shape) |
404 | Not Found | Resource doesn't exist (or hide existence with 403) |
// Minimal auth middleware
function requireAuth(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).json({ error: 'No token' });
try {
req.user = jwt.verify(token, process.env.JWT_SECRET);
next();
} catch {
res.status(403).json({ error: 'Invalid token' });
}
}
// Minimal role check
function requireRole(...roles) {
return (req, res, next) => {
if (!roles.includes(req.user?.role)) {
return res.status(403).json({ error: 'Forbidden' });
}
next();
};
}
// Wire them up
app.get('/admin', requireAuth, requireRole('admin'), handler);