Skip to main content

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.

Sample Application

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.

Live Demo

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 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 1.x Users

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 adapter
  • reflect-metadata (^0.1.13 || ^0.2.0) — NestJS adapter
  • rxjs (^7.0.0) — NestJS adapter

Database driver (choose one):

  • pg (^8.0.0) — PostgreSQL
  • mysql2 (^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/WebAuthn
  • redis (^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:

src/config/auth.config.ts
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​

src/main.ts
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 as 10.0.0.0/8 also works.
  • No proxy: leave TRUST_PROXY unset — req.ip is the socket peer (fail-closed) and forwarding headers are ignored.
  • Never set it to true (trust-all) — that re-enables X-Forwarded-For spoofing.

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. :::

src/app.module.ts
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.

src/config/auth.config.ts
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 themWrite them
Setuproutes: [{ prefix: 'auth' }]Omit routes, declare controllers
EndpointsEvery shipped route for the features you enabledOnly the ones you write
Customisingexclude a key, write that oneFull control everywhere
Keeping upNew endpoints arrive with upgradesYou add them yourself

The routes block above is all there is. AuthModule.forRoot() registers the controllers.

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.

src/config/auth.config.ts
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.

src/config/auth.config.ts
routes: [{ prefix: 'auth', exclude: ['login'] }],
src/auth/login.controller.ts
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.

Administrative routes are opt-in

The admin group is never mounted by default, and mounting it requires an authorization provider — the toolkit ships no role model. See Authorization.

Keep the exception filter

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:

.env
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 install @nauth-toolkit/social-google
src/app.module.ts
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 install @nauth-toolkit/mfa-totp
src/app.module.ts
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.

Complete Route Examples

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-angular with built-in interceptors and guards
  • React Frontend — uses @nauth-toolkit/client with 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