# JWT in Node

December 19, 2025 Javascript Node JWT JSDoc Back-end

Table of content

JWTs are a great alternative to sessions and cookies. This article walks through usage of JWTs in NodeJS.

$ npm i jsonwebtoken zod
$ npm i -D @types/jsonwebtoken
import jwt from "jsonwebtoken";
import { z } from "zod";

/**
 * @typedef {"admin" | "user"} UserRole
 *
 * @typedef User
 * @property {string} user_id
 * @property {string} email
 * @property {UserRole} role
 */

/**
 * @typedef SessionToken
 * @property {string} accessToken
 * @property {string} refreshToken
 * @property {number} expiry
 */

/**
 * @typedef JWTEncoded
 * @property {string} token
 * @property {number} expiry
 */

const SESSION_REFRESH_TOKEN_SCOPE = "session.refresh";
const SESSION_ACCESS_TOKEN_SCOPE = "session.access";

class SessionRefreshToken {
	/** @type {string} */
	userId;

	/** @type {string} */
	email;

	/** @type {UserRole} */
	role;

	/** @type {string} */
	scope;

	/**
	 * @param {string} userId
	 * @param {string} email
	 * @param {UserRole} role
	 */
	constructor(userId, email, role) {
		this.userId = userId;
		this.email = email;
		this.role = role;
		this.scope = SESSION_REFRESH_TOKEN_SCOPE;
	}

	/**
	 * @returns {JWTEncoded}
	 */
	encode() {
		const token = JWT.generateNonExpiringToken({
			userId: this.userId,
			email: this.email,
			role: this.role,
			scope: this.scope,
		});

		return { token, expiry: -1 };
	}

	/**
	 * @param {unknown} payload
	 * @returns {SessionRefreshToken | undefined}
	 */
	static #validatePayload(payload) {
		const schema = z.object({
			userId: z.string().uuid(),
			email: z.string().email(),
			role: z.enum(["admin", "user"]),
			scope: z.literal(SESSION_REFRESH_TOKEN_SCOPE),
		});

		const v = schema.safeParse(payload);
		if (!v.success) {
			return;
		}

		return new SessionRefreshToken(
			v.data.userId,
			v.data.email,
			v.data.role,
		);
	}

	/**
	 * @param {string} token
	 * @return {SessionRefreshToken | undefined}
	 */
	static validate(token) {
		try {
			const payload = jwt.verify(token, config.jwt.secret);
			if (typeof payload == "string") return;
			return SessionRefreshToken.#validatePayload(payload);
		} catch {}
	}
}

class SessionAccessToken extends SessionRefreshToken {
	/** @type {number} */
	expiry;

	/** @type {string} */
	scope;

	/**
	 * @param {string} userId
	 * @param {string} email
	 * @param {UserRole} role
	 */
	constructor(userId, email, role) {
		super(userId, email, role);
		this.scope = SESSION_ACCESS_TOKEN_SCOPE;
		this.expiry = JWT.calculateExpiry();
	}

	/**
	 * @returns {JWTEncoded}
	 */
	enodeWithExpiry() {
		return JWT.generateExpiringToken({
			userId: this.userId,
			email: this.email,
			role: this.role,
			scope: this.scope,
		});
	}

	/**
	 * @param {unknown} payload
	 * @returns {SessionAccessToken | undefined}
	 */
	static #validatePayload(payload) {
		const schema = z.object({
			userId: z.string().uuid(),
			email: z.string().email(),
			role: z.enum(["admin", "user"]),
			scope: z.literal(SESSION_ACCESS_TOKEN_SCOPE),
		});

		const v = schema.safeParse(payload);
		if (!v.success) {
			return;
		}

		return new SessionAccessToken(v.data.userId, v.data.email, v.data.role);
	}

	/**
	 * @param {string} token
	 * @return {SessionAccessToken | undefined}
	 */
	static validate(token) {
		try {
			const payload = jwt.verify(token, config.jwt.secret);
			if (typeof payload == "string") return;
			return SessionAccessToken.#validatePayload(payload);
		} catch {}
	}
}

export class JWTFactory {
	/**
	 * create access and refresh tokens for the user at successful login
	 *
	 * @param {User} user
	 * @returns {SessionToken}
	 */
	static generateSessionToken(user) {
		const accessPayload = new SessionAccessToken(
			user.user_id,
			user.email,
			user.role,
		).enodeWithExpiry();
		const refreshPayload = new SessionRefreshToken(
			user.user_id,
			user.email,
			user.role,
		).encode();

		return {
			accessToken: accessPayload.token,
			expiry: accessPayload.expiry,
			refreshToken: refreshPayload.token,
		};
	}
}

export class JWT {
	/**
	 * calculate expiry timestamp for the client
	 * @returns {number}
	 */
	static calculateExpiry() {
		return (
			Math.floor(Date.now() / 1000) + 60 * 60 * config.jwt.expiresInHours
		);
	}

	/**
	 * @param {object} payload
	 * @returns {JWTEncoded}
	 */
	static generateExpiringToken(payload) {
		const token = jwt.sign(payload, config.jwt.secret, {
			expiresIn: 60 * 60 * config.jwt.expiresInHours,
			notBefore: "0",
			algorithm: config.jwt.algorithm,
			issuer: config.jwt.issuer,
		});

		const expiry = this.calculateExpiry();
		return { token, expiry };
	}

	/**
	 * @param {object} payload
	 * @returns {string}
	 */
	static generateNonExpiringToken(payload) {
		return jwt.sign(payload, config.jwt.secret, {
			notBefore: "0",
			algorithm: config.jwt.algorithm,
			issuer: config.jwt.issuer,
		});
	}
}

const config = {
	jwt: {
		secret: "super-secret-secret",
		expiresInHours: 1,
		algorithm: /** @type {jwt.Algorithm} */ ("HS256"),
		issuer: "host.com",
	},
};

/** @returns {Promise<void>} */
async function main() {
	/** @type {User} */
	const user = {
		user_id: crypto.randomUUID(),
		email: "user@site.com",
		role: "admin",
	};

	const payload = JWTFactory.generateSessionToken(user);

	const accessToken = SessionAccessToken.validate(payload.accessToken);
	console.log(JSON.stringify(accessToken));

	const refreshToken = SessionRefreshToken.validate(payload.refreshToken);
	console.log(JSON.stringify(refreshToken));
}

main();