Build a NestJS project with sign-up, sign-in, sign-out, refresh, and stateless JWT authentication.
This guide walks you through a minimal NestJS application using brkpt-auth.
You will start from a new NestJS project, initialize brkpt-auth, configure JWT authentication, implement the required adapters, and enable account-password authentication with the credentials feature.
By the end of this guide, your application will support:
POST /auth/sign-upPOST /auth/sign-inPOST /auth/sign-outPOST /auth/refreshGET /auth/me
Prerequisites
Section titled “Prerequisites”- Node.js 20+
Create a NestJS project
Section titled “Create a NestJS project”Install the NestJS CLI and create a new project:
pnpm add -g @nestjs/clinpm install -g @nestjs/cliyarn global add @nestjs/cliCreate a new NestJS project:
nest new brkpt-auth-get-started --strictThen enter the project directory:
cd brkpt-auth-get-startedYou can remove the default app.controller.ts, app.controller.spec.ts, and app.service.ts files. This guide does not use them.
Initialize brkpt-auth
Section titled “Initialize brkpt-auth”Install the brkpt CLI globally:
pnpm add -g @brkpt/clinpm install -g @brkpt/cliyarn global add @brkpt/cliRun the initialization command from the project root:
brkpt auth initThe CLI detects your project structure, installs the brkpt-auth source files, and shows any missing dependencies you need to add.
The core feature is added by default. It provides the base JWT flow and shared endpoints such as /auth/me, /auth/refresh, and /auth/sign-out.
Configure BrkptAuthModule
Section titled “Configure BrkptAuthModule”The core feature needs separate configuration for access tokens and refresh tokens.
Install dependencies
Install the dependencies used in this guide:
pnpm add @nestjs/config @nestjs/event-emitter cookie-parserpnpm add -D @types/cookie-parsernpm install @nestjs/config @nestjs/event-emitter cookie-parsernpm install -D @types/cookie-parseryarn add @nestjs/config @nestjs/event-emitter cookie-parseryarn add -D @types/cookie-parserCreate environment variables
Create a .env file in the project root:
JWT_ACCESS_SECRET="your-access-secret"JWT_REFRESH_SECRET="your-refresh-secret"Register BrkptAuthModule
Register ConfigModule, EventEmitterModule, and BrkptAuthModule in app.module.ts:
import { Module } from '@nestjs/common';import { ConfigModule, ConfigService } from '@nestjs/config';import { EventEmitterModule } from '@nestjs/event-emitter';
import { BrkptAuthModule } from './brkpt-auth/brkpt-auth.module';
@Module({ imports: [ ConfigModule.forRoot({ isGlobal: true }), EventEmitterModule.forRoot({ global: true, wildcard: true, }), BrkptAuthModule.forRootAsync({ imports: [ConfigModule], inject: [ConfigService], useFactory: (config: ConfigService) => ({ jwt: { access: { secret: config.getOrThrow('JWT_ACCESS_SECRET'), expiresIn: '5m', }, refresh: { secret: config.getOrThrow('JWT_REFRESH_SECRET'), expiresIn: '1h', transport: 'cookie', }, }, }), }), ],})export class AppModule {}This guide uses cookie transport for refresh tokens. In this mode, brkpt-auth writes the refresh token to an HttpOnly cookie, so the client does not need to handle it manually.
Enable cookie-parser
Enable cookie-parser in main.ts:
import { NestFactory } from '@nestjs/core';import cookieParser from 'cookie-parser';
import { AppModule } from './app.module';
async function bootstrap() { const app = await NestFactory.create(AppModule); app.use(cookieParser()); await app.listen(process.env.PORT ?? 3000);}void bootstrap();Prepare a user module
Section titled “Prepare a user module”brkpt-auth does not require a specific database schema, user model, ORM, or repository structure.
This guide uses a small in-memory repository inside a UserModule so you can complete the authentication flow without setting up a database.
In a real application, your UserModule would usually provide an ORM-backed service or repository, such as a Prisma service, a TypeORM repository, or your own user data access provider.
Create the following files:
Directorysrc/
Directoryuser/
Directoryrepositories/
- memory-user.repository.ts
- user.entity.ts
- user.module.ts
Define the user type
Create user.entity.ts and define the user shape used in this guide:
export interface User { id: number; name: string; email: string; password: string;}brkpt-auth does not require a fixed user shape. This guide uses id, name, email, and password for the example application. The adapters you write later will decide which fields are used in JWT payloads, password validation, and safe user responses.
Create MemoryUserRepository
Create memory-user.repository.ts as a small in-memory repository:
Create a small in-memory repository:
import { Injectable } from '@nestjs/common';
import { User } from '../user.entity';
@Injectable()export class MemoryUserRepository { private users: User[] = []; private currentId = 1;
findOne(predicate: (u: User) => boolean): Promise<User | null> { return Promise.resolve(this.users.find(predicate) || null); }
findAll(): Promise<User[]> { return Promise.resolve([...this.users]); }
create(data: Omit<User, 'id'>): Promise<User> { const newUser: User = { id: this.currentId++, ...data, }; this.users.push(newUser); return Promise.resolve(newUser); }
update(id: number, data: Partial<Omit<User, 'id'>>): Promise<User> { const userIndex = this.users.findIndex((u) => u.id === id); if (userIndex === -1) { throw new Error(`User with id ${id} not found`); } const updated = { ...this.users[userIndex], ...data }; this.users[userIndex] = updated; return Promise.resolve(updated); }
delete(id: number): Promise<User> { const userIndex = this.users.findIndex((u) => u.id === id); if (userIndex === -1) { throw new Error(`User with id ${id} not found`); } const [deleted] = this.users.splice(userIndex, 1); return Promise.resolve(deleted); }}Create UserModule
Create user.module.ts and export MemoryUserRepository so adapters can inject it:
import { Module } from '@nestjs/common';
import { MemoryUserRepository } from './repositories/memory-user.repository';
@Module({ providers: [MemoryUserRepository], exports: [MemoryUserRepository],})export class UserModule {}Connect the core feature
Section titled “Connect the core feature”The core feature handles the base JWT flow, but it does not know what your user model looks like. The core adapter tells brkpt-auth how to map users to JWT payloads, find users from JWT payloads, and return safe user data.
Create the following files:
Directorysrc/
Directorybrkpt-auth/
Directoryadapters/
- core.adapter.ts
- types.ts
Define the JWT payload
Create types.ts:
export type AuthJwtPayload = { sub: number; email: string;};Implement CoreAdapter
Create core.adapter.ts:
import { Injectable } from '@nestjs/common';
import { MemoryUserRepository } from '../../user/repositories/memory-user.repository';import { User } from '../../user/user.entity';import { CorePort } from '../features/core/core.port';import { AuthJwtPayload } from './types';
@Injectable()export class CoreAdapter implements CorePort<User> { constructor(private readonly userRepo: MemoryUserRepository) {}
mapUserToJwtPayload(user: User): AuthJwtPayload { return { sub: user.id, email: user.email, }; }
shrinkJwtPayload(payload: AuthJwtPayload): Record<string, unknown> { return { sub: payload.sub, }; }
findUserByJwtPayload(payload: AuthJwtPayload): Promise<User | null> { return this.userRepo.findOne((u) => u.id === payload.sub); }
toSafeUser(user: User): Record<string, unknown> { const { password: _password, ...safe } = user; return safe; }
extractUserIdFromJwtPayload(payload: AuthJwtPayload): number { return payload.sub; }}Each method is a small mapping or a direct call to your user repository.
Register the feature
Update features.ts and pass CoreAdapter to coreFeature:
import { CoreAdapter } from './adapters/core.adapter';import { FeatureConfig } from './common/interfaces';import { coreFeature } from './features/core/core.feature';
export const features: FeatureConfig[] = [coreFeature(CoreAdapter)];Import UserModule
CoreAdapter depends on MemoryUserRepository, which is exported by UserModule.
Add UserModule to brkpt-auth.module.ts:
import { UserModule } from '../user/user.module';
@Module({ imports: [], imports: [UserModule], controllers: [...features.flatMap((f) => f.controllers)], providers: [...features.flatMap((f) => f.providers)],})export class BrkptAuthModule { // ...}At this point, the core feature can resolve users from JWT payloads and return safe user data.
If you start the application now and call GET /auth/me, it should return 401 Unauthorized. This is expected because the application does not have sign-up or sign-in endpoints yet.
Add the credentials feature
Section titled “Add the credentials feature”The core feature does not include account registration or sign-in. This is intentional: you can choose the sign-in methods your application needs.
This guide uses the credentials feature for account-password authentication.
Add the feature
Run the CLI command:
brkpt auth add credentialsThe CLI adds a new features/credentials/ folder.
Define the DTOs
The credentials feature generates empty DTO classes by default. brkpt-auth does not assume which fields your sign-up and sign-in requests use.
Update sign-up.dto.ts:
export class SignUpDto { name!: string; email!: string; password!: string;}Update sign-in.dto.ts:
export class SignInDto { email!: string; password!: string;}Install bcrypt
This guide uses bcrypt to hash and verify passwords:
pnpm add bcryptpnpm add -D @types/bcryptnpm install bcryptnpm install -D @types/bcryptyarn add bcryptyarn add -D @types/bcryptImplement CredentialsAdapter
Create credentials.adapter.ts:
import { Injectable } from '@nestjs/common';import * as bcrypt from 'bcrypt';
import { MemoryUserRepository } from '../../user/repositories/memory-user.repository';import { User } from '../../user/user.entity';import { CredentialsPort } from '../features/credentials/credentials.port';import { SignInDto } from '../features/credentials/dto/sign-in.dto';import { SignUpDto } from '../features/credentials/dto/sign-up.dto';
@Injectable()export class CredentialsAdapter implements CredentialsPort<User> { constructor(private readonly userRepo: MemoryUserRepository) {}
findUserByDto(dto: SignInDto | SignUpDto): Promise<User | null> { return this.userRepo.findOne((u) => u.email === dto.email); }
validatePassword(user: User, dto: SignInDto): Promise<boolean> { return bcrypt.compare(dto.password, user.password); }
async createUser(dto: SignUpDto): Promise<User> { const password = await bcrypt.hash(dto.password, 10); return this.userRepo.create({ name: dto.name, email: dto.email, password: password, }); }
extractUserIdFromUser(user: User): number { return user.id; }}The password strategy is fully controlled by the adapter. You can replace bcrypt with argon2 or any other hashing library by changing validatePassword and createUser.
Register the feature
Update features.ts and pass CredentialsAdapter to credentialsFeature:
import { CoreAdapter } from './adapters/core.adapter';import { CredentialsAdapter } from './adapters/credentials.adapter';import { FeatureConfig } from './common/interfaces';import { coreFeature } from './features/core/core.feature';import { credentialsFeature } from './features/credentials/credentials.feature';
export const features: FeatureConfig[] = [ coreFeature(CoreAdapter), credentialsFeature(CredentialsAdapter),];CredentialsAdapter uses the same MemoryUserRepository, so no additional module import is needed.
Validate request data
Section titled “Validate request data”brkpt-auth does not force a validation library. This guide uses the standard NestJS ValidationPipe with class-validator.
Install validation dependencies
Install the dependencies:
pnpm add class-validator class-transformernpm install class-validator class-transformeryarn add class-validator class-transformerEnable ValidationPipe
Enable the global validation pipe in main.ts:
import { ValidationPipe } from '@nestjs/common';import { NestFactory } from '@nestjs/core';import cookieParser from 'cookie-parser';
import { AppModule } from './app.module';
async function bootstrap() { const app = await NestFactory.create(AppModule); app.use(cookieParser()); app.useGlobalPipes( new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true, }), ); await app.listen(process.env.PORT ?? 3000);}void bootstrap();Validate the DTOs
Add validation decorators to sign-up.dto.ts:
import { IsEmail, IsString, MinLength } from 'class-validator';
export class SignUpDto { @IsString() name!: string;
@IsEmail() email!: string;
@IsString() @MinLength(6) password!: string;}Add validation decorators to sign-in.dto.ts:
import { IsEmail, IsString } from 'class-validator';
export class SignInDto { @IsEmail() email!: string;
@IsString() password!: string;}Run and verify
Section titled “Run and verify”Start the application:
pnpm start:devnpm run start:devyarn start:devThe following endpoints are now available:
| Method | Path | Description |
|---|---|---|
POST |
/auth/sign-up |
Create a user |
POST |
/auth/sign-in |
Sign in |
POST |
/auth/sign-out |
Sign out |
POST |
/auth/refresh |
Issue a new access token |
GET |
/auth/me |
Return the current user |
Use any HTTP client, such as Postman, Bruno, or Insomnia, to test the endpoints.
Sign up
Send a sign-up request:
POST /auth/sign-upContent-Type: application/json
{ "name": "Kevin", "email": "kevin@example.com", "password": "password123"}The response includes an accessToken and sets the refresh token as an HttpOnly cookie.
Sign in
Send a sign-in request with the same account:
POST /auth/sign-inContent-Type: application/json
{ "email": "kevin@example.com", "password": "password123"}Like sign-up, the response includes an accessToken and sets the refresh token as an HttpOnly cookie.
Get the current user
Call /auth/me with the access token:
GET /auth/meAuthorization: Bearer <access-token>The response returns the current user without the password field.
Refresh the access token
Call /auth/refresh with the refresh token cookie set by sign-up or sign-in:
POST /auth/refreshCookie: refreshToken=<refresh-token>The response includes a new accessToken.
Sign out
Call /auth/sign-out with the access token:
POST /auth/sign-outAuthorization: Bearer <access-token>In cookie transport mode, sign-out clears the refresh token cookie if it is present.
Next steps
Section titled “Next steps”Continue through the guides in order to replace the in-memory repository, add session management, and extend your authentication flow feature by feature.