Authentication Middleware
The authentication middleware is the gatekeeper of your API. It runs before protected route handlers and ensures the request comes from a valid, authenticated user.
The authenticate middleware
Section titled “The authenticate middleware”import { Request, Response, NextFunction } from 'express'import { verifyToken } from '../utils/jwt.js'
export function authenticate(req: Request, res: Response, next: NextFunction) { const authHeader = req.headers.authorization
if (!authHeader || !authHeader.startsWith('Bearer ')) { return res.status(401).json({ error: 'Authorization token required' }) }
const token = authHeader.split(' ')[1]
try { const payload = verifyToken(token) res.locals.user = payload // { userId, username } next() } catch { res.status(401).json({ error: 'Invalid or expired token' }) }}After this middleware runs, res.locals.user contains { userId, username } — available in any downstream handler.
Applying to routes
Section titled “Applying to routes”import { Router } from 'express'import { authenticate } from '../middleware/authenticate.js'import { listPosts, getPost, createPost, deletePost } from '../controllers/posts.js'
const router = Router()
router.get('/', listPosts) // public — no auth requiredrouter.get('/:id', getPost) // publicrouter.post('/', authenticate, createPost) // requires authrouter.delete('/:id', authenticate, deletePost) // requires auth
export default routerApply authenticate only to routes that need it. Public routes (listing posts, reading post details) don’t require authentication.
Using the user in a controller
Section titled “Using the user in a controller”export function createPost(req: Request, res: Response) { const { title, body } = req.body const { userId } = res.locals.user // from authenticate middleware
const result = db.prepare( 'INSERT INTO posts (title, body, user_id) VALUES (?, ?, ?)' ).run(title, body, userId)
const post = db.prepare('SELECT * FROM posts WHERE id = ?') .get(result.lastInsertRowid)
res.status(201).json(post)}
export function deletePost(req: Request, res: Response) { const postId = parseInt(req.params.id) const { userId } = res.locals.user // only delete if it's your post
const result = db.prepare( 'DELETE FROM posts WHERE id = ? AND user_id = ?' ).run(postId, userId)
if (result.changes === 0) { return res.status(404).json({ error: 'Post not found or not yours' }) }
res.sendStatus(204)}The AND user_id = ? in the DELETE statement handles authorization automatically — you can only delete your own posts.
TypeScript: extending res.locals
Section titled “TypeScript: extending res.locals”For full type safety, extend Express’s Locals interface:
import { TokenPayload } from '../utils/jwt.js'
declare global { namespace Express { interface Locals { user?: TokenPayload } }}Now res.locals.user has the correct type throughout the codebase.
Optional authentication
Section titled “Optional authentication”Some routes behave differently for authenticated vs anonymous users:
export function optionalAuth(req: Request, res: Response, next: NextFunction) { const authHeader = req.headers.authorization if (!authHeader) return next() // anonymous is fine
const token = authHeader.split(' ')[1] try { res.locals.user = verifyToken(token) } catch { // ignore invalid token for optional auth } next()}Use this on public routes where you want to know if the user is logged in (e.g., to show “upvoted” state) but don’t require it.
Exercise
Section titled “Exercise”- Create
src/middleware/authenticate.ts. - Apply it to
POST /postsandDELETE /posts/:id. - Test without a token — confirm 401.
- Test with a valid token — confirm the route runs.
- Add
src/types/express.d.tsfor TypeScript type safety.
- The
authenticatemiddleware readsAuthorization: Bearer <token>, verifies the JWT, and attachespayloadtores.locals.user. - Return 401 if the token is missing, malformed, or expired.
- Apply
authenticateonly to routes that require it — use it as a per-route argument. - Use
AND user_id = ?in UPDATE/DELETE queries to enforce ownership (authorization).