Skip to content

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.

src/middleware/authenticate.ts
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.

src/routes/posts.ts
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 required
router.get('/:id', getPost) // public
router.post('/', authenticate, createPost) // requires auth
router.delete('/:id', authenticate, deletePost) // requires auth
export default router

Apply authenticate only to routes that need it. Public routes (listing posts, reading post details) don’t require authentication.

src/controllers/posts.ts
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.

For full type safety, extend Express’s Locals interface:

src/types/express.d.ts
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.

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.

  1. Create src/middleware/authenticate.ts.
  2. Apply it to POST /posts and DELETE /posts/:id.
  3. Test without a token — confirm 401.
  4. Test with a valid token — confirm the route runs.
  5. Add src/types/express.d.ts for TypeScript type safety.
  • The authenticate middleware reads Authorization: Bearer <token>, verifies the JWT, and attaches payload to res.locals.user.
  • Return 401 if the token is missing, malformed, or expired.
  • Apply authenticate only to routes that require it — use it as a per-route argument.
  • Use AND user_id = ? in UPDATE/DELETE queries to enforce ownership (authorization).