Quick Start: NestJS
Add authentication to a NestJS application using nauth-toolkit. By the end of this guide you'll have signup, login, logout, and a protected route working.
A complete working example is available at github.com/noorixorg/nauth-toolkit/tree/main/examples/starter-nestjs — NestJS + TypeORM + PostgreSQL with lifecycle hooks, custom email templates, Google OAuth, and TOTP MFA preconfigured. A more extensive demo app lives at examples/demo-nestjs.
Try the finished result at demo.nauth.dev — a working app built on nauth-toolkit showcasing signup, login, MFA, social auth, and session management.
Prerequisites
- Node.js 22+ — Node 24 recommended (older versions may work but are untested)
- PostgreSQL or MySQL database running
- A NestJS application (or
npx @nestjs/cli new my-app)
Installation
This example uses PostgreSQL. For MySQL, replace @nauth-toolkit/database-typeorm-postgres with @nauth-toolkit/database-typeorm-mysql and pg with mysql2.
- npm
- Yarn
- pnpm
- Bun
npm install @nauth-toolkit/core @nauth-toolkit/nestjs @nauth-toolkit/database-typeorm-postgres @nauth-toolkit/storage-database @nauth-toolkit/email-console @nauth-toolkit/sms-console cookie-parser dotenv
yarn add @nauth-toolkit/core @nauth-toolkit/nestjs @nauth-toolkit/database-typeorm-postgres @nauth-toolkit/storage-database @nauth-toolkit/email-console @nauth-toolkit/sms-console cookie-parser dotenv
pnpm add @nauth-toolkit/core @nauth-toolkit/nestjs @nauth-toolkit/database-typeorm-postgres @nauth-toolkit/storage-database @nauth-toolkit/email-console @nauth-toolkit/sms-console cookie-parser dotenv
bun add @nauth-toolkit/core @nauth-toolkit/nestjs @nauth-toolkit/database-typeorm-postgres @nauth-toolkit/storage-database @nauth-toolkit/email-console @nauth-toolkit/sms-console cookie-parser dotenv
Yarn 1.x doesn't auto-install peer dependencies. You'll need to also run:
yarn add typeorm @nestjs/common @nestjs/core @nestjs/typeorm reflect-metadata rxjs pg cookie-parser
# For MySQL instead: yarn add mysql2 (and replace pg)
npm, pnpm, bun, and Yarn 2+ handle peer dependencies automatically.
Peer Dependencies
These are installed automatically by npm, pnpm, bun, and Yarn 2+. Only Yarn 1.x users need to install them manually:
Required:
typeorm(^0.3.0) — database adapter@nestjs/common(^9.0.0 || ^10.0.0 || ^11.0.0) — NestJS adapter@nestjs/core(^9.0.0 || ^10.0.0 || ^11.0.0) — NestJS adapter@nestjs/typeorm(^9.0.0 || ^10.0.0 || ^11.0.0) — NestJS adapterreflect-metadata(^0.1.13 || ^0.2.0) — NestJS adapterrxjs(^7.0.0) — NestJS adapter
Database driver (choose one):
pg(^8.0.0) — PostgreSQLmysql2(^2.3.0 || ^3.0.0) — MySQL
Optional (for specific features):
qrcode(^1.5.3) — MFA TOTP QR code generation@simplewebauthn/server(^13.2.2) — MFA Passkey/WebAuthnredis(^4.6.0 || ^5.0.0) — Redis storage adapter@maxmind/geoip2-node(^4.0.0 || ^5.0.0 || ^6.0.0) — IP geolocation
Configuration
Create the auth configuration file:
import { Logger } from '@nestjs/common';
import { NAuthModuleConfig, createDatabaseStorageAdapter } from '@nauth-toolkit/nestjs';
import { ConsoleEmailProvider } from '@nauth-toolkit/email-console';
import { ConsoleSMSProvider } from '@nauth-toolkit/sms-console';
export const authConfig: NAuthModuleConfig = {
jwt: {
algorithm: 'HS256',
accessToken: {
secret: process.env.JWT_SECRET as string,
expiresIn: '15m',
},
refreshToken: {
secret: process.env.JWT_REFRESH_SECRET as string,
expiresIn: '7d',
rotation: true,
},
},
logger: {
instance: new Logger('NAuth'),
enablePiiRedaction: true, // Redact emails, IPs, tokens
},
password: {
minLength: 8,
requireUppercase: true,
requireNumbers: true,
requireSpecialChars: true,
},
// Uses your TypeORM database for transient storage (rate limits, locks, etc.)
// For multi-server production deployments, use createRedisStorageAdapter() instead
storageAdapter: createDatabaseStorageAdapter(),
// Console providers log to stdout — replace with real providers for production
emailProvider: new ConsoleEmailProvider(),
smsProvider: new ConsoleSMSProvider(),
signup: {
enabled: true,
// Set to 'email', 'phone', or 'both' to require verification via the Challenge System
verificationMethod: 'none',
},
} satisfies NAuthModuleConfig;
Bootstrap
import { NestFactory } from '@nestjs/core';
import { ExpressAdapter } from '@nestjs/platform-express';
import { NAuthHttpExceptionFilter, NAuthValidationPipe } from '@nauth-toolkit/nestjs';
import * as dotenv from 'dotenv';
import cookieParser from 'cookie-parser';
// Load environment variables BEFORE importing AppModule
dotenv.config();
import { AppModule } from './app.module';
async function bootstrap(): Promise<void> {
const app = await NestFactory.create(AppModule, new ExpressAdapter());
// Trust proxy — so req.ip is the real client behind a reverse proxy / load balancer
// (see the admonition below). Skip when nothing proxies you.
if (process.env.TRUST_PROXY && process.env.TRUST_PROXY !== 'false') {
const tp = process.env.TRUST_PROXY;
app.getHttpAdapter().getInstance().set('trust proxy', /^\d+$/.test(tp) ? parseInt(tp, 10) : tp);
}
// Required for cookie-based token delivery
app.use(cookieParser());
app.useGlobalFilters(new NAuthHttpExceptionFilter());
app.useGlobalPipes(new NAuthValidationPipe());
// Adjust origins for your frontend — required for credential-bearing cross-origin requests
app.enableCors({
origin: ['http://localhost:4200', 'http://localhost:5173'],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Device-Id', 'x-csrf-token', 'x-device-token'],
});
await app.listen(3000);
}
bootstrap();
:::warning Configure trust proxy behind a proxy or load balancer
nauth-toolkit keys IP-based account lockout, geolocation, and audit on req.ip. req.ip is only correct when Express's trust proxy matches your topology; otherwise every client collapses to the proxy's IP, and — worse, if you trust too broadly — a forged X-Forwarded-For can spoof the IP.
- One reverse proxy (Caddy/Nginx on dev/staging):
TRUST_PROXY=1. - AWS ELB/ALB (ECS, Copilot) or other private-network load balancers:
TRUST_PROXY=uniquelocal— trusts loopback + private ranges, so the first public hop from the right (the real client) wins regardless of how many private hops (LB, sidecar) precede it. A specific CIDR such as10.0.0.0/8also works. - No proxy: leave
TRUST_PROXYunset —req.ipis the socket peer (fail-closed) and forwarding headers are ignored. - Never set it to
true(trust-all) — that re-enablesX-Forwarded-Forspoofing.
Ensure the app is only reachable through the proxy (network isolation/security groups); a hop count like 1 assumes exactly that many proxies always sit in front.
:::
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthModule } from '@nauth-toolkit/nestjs';
import { getNAuthEntities, getNAuthTransientStorageEntities } from '@nauth-toolkit/database-typeorm-postgres';
// For MySQL: import { getNAuthEntities, getNAuthTransientStorageEntities } from '@nauth-toolkit/database-typeorm-mysql';
import { authConfig } from './config/auth.config';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'postgres', // For MySQL: 'mysql'
host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT || '5432'), // For MySQL: '3306'
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
// Storage entities are required when using DatabaseStorageAdapter.
// If you use RedisStorageAdapter instead, you can omit getNAuthTransientStorageEntities().
entities: [...getNAuthEntities(), ...getNAuthTransientStorageEntities()],
synchronize: false, // nauth-toolkit runs its own migrations on startup
}),
AuthModule.forRoot(authConfig),
],
})
export class AppModule {}
Auth Routes
Add a routes block to your config. AuthModule.forRoot() mounts every shipped endpoint —
sign-up, sign-in, refresh, challenges, profile, MFA, social linking, sessions and device trust —
so there is no controller to write.
export const authConfig: NAuthModuleConfig = {
// ...jwt, providers, and the rest of the Configuration section
routes: [{ prefix: 'auth' }],
};
That serves the paths the frontend SDK expects by default, so no endpoint overrides are needed on the client.
Two ways to get routes
Mounting is optional. Everything the toolkit does is available either way — the shipped routes are thin wrappers over the same services you would call yourself.
| Mount them | Write them | |
|---|---|---|
| Setup | routes: [{ prefix: 'auth' }] | Omit routes, declare controllers |
| Endpoints | Every shipped route for the features you enabled | Only the ones you write |
| Customising | exclude a key, write that one | Full control everywhere |
| Keeping up | New endpoints arrive with upgrades | You add them yourself |
- Mount them
- Write your own
The routes block above is all there is. AuthModule.forRoot() registers the controllers.
Leave routes out of the config entirely — no endpoints are registered — and declare a
controller. Apply AuthGuard at class level so routes are
protected by default, and @Public() to opt out.
import { Controller, Post, Get, Body, HttpCode, HttpStatus, UseGuards } from '@nestjs/common';
import {
AuthService,
SignupDTO,
LoginDTO,
AuthResponseDTO,
UserResponseDTO,
AuthGuard,
CurrentUser,
Public,
IUser,
} from '@nauth-toolkit/nestjs';
@UseGuards(AuthGuard)
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Public()
@Post('signup')
@HttpCode(HttpStatus.CREATED)
async signup(@Body() dto: SignupDTO): Promise<AuthResponseDTO> {
return this.authService.signup(dto);
}
@Public()
@Post('login')
@HttpCode(HttpStatus.OK)
async login(@Body() dto: LoginDTO): Promise<AuthResponseDTO> {
return this.authService.login(dto);
}
@Get('profile')
async getProfile(@CurrentUser() user: IUser): Promise<UserResponseDTO> {
return UserResponseDTO.fromEntity(user);
}
}
@Module({
imports: [/* ... */ AuthModule.forRoot(authConfig)],
controllers: [AuthController],
})
export class AppModule {}
Basic Auth Flows covers every endpoint in this form — request shapes, responses and the challenge loop.
Serving web and mobile from one backend
groups selects which bundles to mount — Route Groups lists what each one contains.
Mount the same routes twice, differing only in how tokens travel. This needs
tokenDelivery.method: 'hybrid'; the toolkit refuses at startup otherwise.
tokenDelivery: { method: 'hybrid' },
routes: [
// Web: httpOnly cookies, CSRF.
{ prefix: 'auth', delivery: 'cookies' },
// Native and mobile clients: tokens in the response body, no CSRF.
{ prefix: 'mobile/auth', delivery: 'json', groups: ['core', 'profile', 'mfa', 'social', 'device'] },
],
Customising one route
Exclude its key and write only that route. Everything else stays mounted.
routes: [{ prefix: 'auth', exclude: ['login'] }],
import { Controller, Post, Body, HttpCode } from '@nestjs/common';
import { AuthService, LoginDTO, AuthResponseDTO, Public } from '@nauth-toolkit/nestjs';
@Controller('auth')
export class LoginController {
constructor(private readonly authService: AuthService) {}
@Public()
@Post('login')
@HttpCode(200)
async login(@Body() dto: LoginDTO): Promise<AuthResponseDTO> {
// your own logic here, then delegate
return this.authService.login(dto);
}
}
Register it in controllers as usual. An unknown key in exclude throws at startup, so a typo
cannot silently leave the shipped route in place.
The admin group is never mounted by default, and mounting it requires an authorization
provider — the toolkit ships no role model. See Authorization.
NAuthHttpExceptionFilter from the Bootstrap step maps toolkit errors to their HTTP status
codes. Without it, validation failures on the mounted routes surface as 500 instead of 400.
Environment Variables
Create a .env file in your project root:
DB_HOST=localhost
DB_PORT=5432
DB_USERNAME=postgres
DB_PASSWORD=postgres
DB_DATABASE=myapp
JWT_SECRET=your-access-token-secret-min-32-chars
JWT_REFRESH_SECRET=your-refresh-token-secret-min-32-chars
Verify the Backend
Start your application and test the endpoints:
# Signup
curl -X POST http://localhost:3000/auth/signup \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com", "password": "MyPassword1!"}'
# Login
curl -X POST http://localhost:3000/auth/login \
-H "Content-Type: application/json" \
-d '{"identifier": "user@example.com", "password": "MyPassword1!"}'
# Profile (use the accessToken from the login response)
curl http://localhost:3000/auth/profile \
-H "Authorization: Bearer <accessToken>"
You should receive an AuthResponseDTO containing accessToken, refreshToken, and user from signup and login. The profile endpoint returns the authenticated user object.
Adding Features
Social Login
- npm
- Yarn
- pnpm
- Bun
npm install @nauth-toolkit/social-google
yarn add @nauth-toolkit/social-google
pnpm add @nauth-toolkit/social-google
bun add @nauth-toolkit/social-google
import { GoogleSocialAuthModule } from '@nauth-toolkit/social-google/nestjs';
@Module({
imports: [AuthModule.forRoot(authConfig), GoogleSocialAuthModule],
})
export class AppModule {}
Configure the provider in your auth config — see the Social Login guide for complete setup.
MFA (TOTP)
- npm
- Yarn
- pnpm
- Bun
npm install @nauth-toolkit/mfa-totp
yarn add @nauth-toolkit/mfa-totp
pnpm add @nauth-toolkit/mfa-totp
bun add @nauth-toolkit/mfa-totp
import { TOTPMFAModule } from '@nauth-toolkit/mfa-totp/nestjs';
@Module({
imports: [AuthModule.forRoot(authConfig), TOTPMFAModule],
})
export class AppModule {}
Configure MFA settings in your auth config — see the MFA guide for complete setup.
This quick start covers the minimal setup. For complete route implementations including password reset, MFA verification, social login callbacks, and challenge handling, see the Authentication Routes guide.
Frontend Quick Starts
Now connect a frontend to this backend:
- Angular Frontend — uses
@nauth-toolkit/client-angularwith built-in interceptors and guards - React Frontend — uses
@nauth-toolkit/clientwith a custom Auth context
What's Next
- NestJS Starter App — The code this guide is based on, with hooks, custom email templates, Google OAuth, and MFA
- Configuration — Full configuration reference with all available options
- Challenge System — Understand multi-step authentication flows (MFA, verification)
- Authentication Routes — Complete route implementations for all auth flows
- Token Delivery — Switch between JSON (Bearer) and cookie-based token delivery