NestJS JWT guard testing: working versions (tested 2026-10-18)
Issue and verify JWTs with @nestjs/jwt, protect a route with a Bearer-token AuthGuard, and assert 401 without a token and 200 with a valid one using supertest offline.
passed2 passed · 0 failed2026-10-18 03:58 UTC- Status
- passed
- Last passed
- 2026-10-18 03:58 UTC
- Tests
- 2 / 2
- Language
- TypeScript
- Runtime
- node v22.23.3
npm install --no-audit --no-fund --ignore-scripts=false npm test
node v22.23.3 linux 6.17.0-1022-azure x64 runner: github-actions 39095442401
The code
Exactly the files that passed. Your agent can pull them with get_recipe("nestjs-jwt-guard-testing") (install), or see them on GitHub.
{
"name": "nestjs-jwt-guard-testing",
"private": true,
"type": "module",
"scripts": {
"test": "node --import tsx --test test/*.test.ts"
},
"dependencies": {
"@nestjs/common": "12.1.3",
"@nestjs/core": "12.1.3",
"@nestjs/jwt": "12.0.3",
"@nestjs/platform-express": "12.1.3",
"reflect-metadata": "0.2.2",
"rxjs": "7.8.2"
},
"devDependencies": {
"@nestjs/testing": "12.1.3",
"@types/node": "26.6.5",
"@types/supertest": "7.2.1",
"supertest": "7.3.1",
"tsx": "4.23.15"
}
}
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"experimentalDecorators": true,
"emitDecoratorMetadata": false,
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"allowImportingTsExtensions": true,
"noEmit": true,
"types": ["node"]
},
"include": ["src", "test"]
}
import { Inject, Injectable } from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
export type SessionUser = { sub: string; username: string };
@Injectable()
export class AuthService {
constructor(@Inject(JwtService) private readonly jwt: JwtService) {}
login(username: string): { access_token: string } {
const payload: SessionUser = { sub: username, username };
return { access_token: this.jwt.sign(payload) };
}
verify(token: string): SessionUser {
return this.jwt.verify<SessionUser>(token);
}
}
import {
CanActivate,
ExecutionContext,
Inject,
Injectable,
UnauthorizedException,
} from "@nestjs/common";
import { AuthService, type SessionUser } from "./auth.service.ts";
type BearerRequest = {
headers: { authorization?: string };
user?: SessionUser;
};
@Injectable()
export class AuthGuard implements CanActivate {
constructor(@Inject(AuthService) private readonly auth: AuthService) {}
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest<BearerRequest>();
const header = request.headers.authorization;
if (typeof header !== "string" || !header.startsWith("Bearer ")) {
throw new UnauthorizedException("Missing bearer token");
}
const token = header.slice("Bearer ".length).trim();
try {
request.user = this.auth.verify(token);
return true;
} catch {
throw new UnauthorizedException("Invalid bearer token");
}
}
}
import { Body, Controller, Get, Inject, Post, Request, UseGuards } from "@nestjs/common";
import { AuthService, type SessionUser } from "./auth.service.ts";
import { AuthGuard } from "./auth.guard.ts";
@Controller()
export class AuthController {
constructor(@Inject(AuthService) private readonly auth: AuthService) {}
@Post("login")
login(@Body("username") username: string): { access_token: string } {
return this.auth.login(username);
}
@UseGuards(AuthGuard)
@Get("profile")
profile(@Request() request: { user?: SessionUser }): { user: SessionUser | undefined } {
return { user: request.user };
}
}
import { Module } from "@nestjs/common";
import { JwtModule } from "@nestjs/jwt";
import { AuthService } from "./auth.service.ts";
import { AuthController } from "./auth.controller.ts";
import { AuthGuard } from "./auth.guard.ts";
@Module({
imports: [
JwtModule.register({
secret: "provenpin-test-secret",
signOptions: { expiresIn: "1h" },
}),
],
controllers: [AuthController],
providers: [AuthService, AuthGuard],
})
export class AppModule {}
import "reflect-metadata";
import { test } from "node:test";
import assert from "node:assert/strict";
import request from "supertest";
import { Test } from "@nestjs/testing";
import type { INestApplication } from "@nestjs/common";
import { AppModule } from "../src/app.module.ts";
async function createApp(): Promise<INestApplication> {
const moduleRef = await Test.createTestingModule({ imports: [AppModule] }).compile();
const app = moduleRef.createNestApplication();
await app.init();
return app;
}
test("protected route returns 401 without a token", async (t) => {
const app = await createApp();
t.after(async () => {
await app.close();
});
await request(app.getHttpServer()).get("/profile").expect(401);
});
test("login issues a JWT and the guard accepts it", async (t) => {
const app = await createApp();
t.after(async () => {
await app.close();
});
const login = await request(app.getHttpServer())
.post("/login")
.send({ username: "ada" })
.expect(201);
assert.equal(typeof login.body.access_token, "string");
assert.ok(login.body.access_token.length > 0);
const profile = await request(app.getHttpServer())
.get("/profile")
.set("Authorization", `Bearer ${login.body.access_token}`)
.expect(200);
assert.equal(profile.body.user.username, "ada");
});
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.
Related stacks
NestJS Zod ValidationPipe for body DTOs
Validate NestJS request bodies with a reusable Zod ValidationPipe that returns 400 field errors, tested through @nestjs/testing and supertest without relying on emitDecoratorMetadata.
cors express preflight
Configure cors with express to reflect allowed origins, answer OPTIONS preflight with Access-Control-Allow-* headers, and deny a disallowed origin, tested offline with supertest.
express 5 async error handling
Use express 5 with supertest to forward rejected async handlers to a JSON error middleware, with router-level params, a JSON 404 handler and offline tests.
express-openapi-validator request and response validation
Validate an Express app against an inline OpenAPI 3 spec using express-openapi-validator, returning validation errors as JSON and testing valid and invalid requests offline with supertest.
All @nestjs/core recipesAll @nestjs/common recipesAll @nestjs/jwt recipesAll @nestjs/testing recipesAll @nestjs/platform-express recipesAll supertest recipes