JWT Auth in Express with TS This article provides a tutorial on implementing JWT authentication in an Express.js application using TypeScript, covering the setup of environment variables, custom helper classes for standardized API responses and error handling, and a Mongoose user model with built-in methods for password hashing and token generation. The tutorial emphasizes best practices such as using both access and refresh tokens, attaching authentication methods directly to the Mongoose schema, and using bcrypt for secure password storage. ⚑ Quick Architecture Cheat Sheet For Fast Revision If you are using this post to refresh your memory, here is the core token blueprint: | Token Type | Stored In | Lifetime Recommended | Primary Purpose | |---|---|---|---| Access Token | HTTP-Only Cookie / Auth Header | 15 Minutes | Authenticating short-lived protected route requests | Refresh Token | Database & HTTP-Only Cookie | 7 to 10 Days | Requesting a brand new Access Token when it expires | The Token Lifecycle Flow php Client -------------- 1. Send Login Credentials --------------- Backend Client <-------- 2. Set Access & Refresh Cookies --------------- Backend Saves Refresh Token to DB Client -------- 3. Access Protected Route With Cookie -------- verifyJWT Middleware - Granted Project Setup & Prerequisites πŸ“‚ Project Structure └── src/ β”œβ”€β”€ @types/ β”œβ”€β”€ controllers/ β”œβ”€β”€ middlewares/ β”œβ”€β”€ models/ β”œβ”€β”€ routes/ β”œβ”€β”€ utils/ β”œβ”€β”€ app.ts └── index.ts πŸ“₯ Install Required Packages Run the following commands to install the necessary production and development dependencies: npm install jsonwebtoken bcrypt mongoose cookie-parser npm install -D @types/jsonwebtoken @types/bcrypt 🌐 Environment Variables Create a .env file in your root directory: ACCESS TOKEN SECRET=your super secret access key ACCESS TOKEN EXPIRY=15m REFRESH TOKEN SECRET=your super secret refresh key REFRESH TOKEN EXPIRY=7d Step 1: Standardizing Global API Responses Before touching authentication, we build reusable helper classes. This keeps frontend handling much cleaner since every response follows the same structure. export class ApiError extends Error { statusCode: number; message: string; errors: any ; stack?: string; data: any; success: boolean; constructor statusCode: number, message = "Something went wrong", errors = , stack = "" { super message this.statusCode = statusCode this.data = null this.message = message this.success = false this.errors = errors if stack { this.stack = stack } else { Error.captureStackTrace this, this.constructor } } } export class ApiResponse { statusCode: number; data: any; message: string; success: boolean; constructor statusCode: number, data: any, message: string = "Success" { this.statusCode = statusCode this.data = data this.message = message this.success = statusCode < 400 } } πŸ’‘ Why use this pattern? Instead of manually typing return res.status 200 .json { success: true, data } in dozens of locations, these utility classes enforce structural consistency across your entire backend footprint. Step 2: Designing the Data Layer The User Model Instead of hashing passwords manually inside controllers every time, we move the logic into the schema itself using pre-save hooks and custom schema methods. python import { model, Schema, type HydratedDocument } from "mongoose"; import jwt, { type Secret, type SignOptions } from "jsonwebtoken"; import bcrypt from "bcrypt"; export interface IUser { username: string; fullName: string; email: string; password: string; avatarUrl?: string; refreshToken?: string; generateAccessToken: = string; generateRefreshToken: = string; isPasswordCorrect: password: string = Promise