Overview
NestJS is a modular server framework for Node.js and TypeScript: feature modules, constructor-based dependency injection, decorators for routes and validation, guards for authorization, pipes for input, interceptors and filters for cross-cutting concerns. The current major version is 12 (checked against @nestjs/core 12.2). Its packages are ESM-only, and a new project can be ESM (Vitest tests) or CommonJS (Jest tests). The examples below were compiled against a project generated by the 12 CLI with @nestjs/typeorm 12, typeorm 1.1 and @nestjs/jwt 12.
Instructions
Step 1: Create the project
npm i -g @nestjs/cli
nest new newsdesk-api --package-manager npm
cd newsdesk-api
npm i @nestjs/typeorm typeorm pg @nestjs/jwt @nestjs/config class-validator class-transformer
nest new asks whether the project is CommonJS or ESM (it picks ESM when run without a terminal). Running the CLI generators needs Node.js 22.22.3+, 24.15+ or 26+; running the finished application needs 20.19+ (or 22.12+). The examples use the ESM layout, where relative imports end in .js; in a CommonJS project drop the .js suffix and use jest.fn() instead of vi.fn().
nest g resource articles scaffolds a module, controller, service and DTOs.
Step 2: Entity and DTO
// src/articles/entities/article.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } from 'typeorm';
@Entity()
export class Article {
@PrimaryGeneratedColumn()
id: number;
@Column({ length: 200 })
title: string;
@Column('text')
body: string;
@CreateDateColumn()
createdAt: Date;
}
// src/articles/dto/create-article.dto.ts
import { IsString, IsNotEmpty, MaxLength } from 'class-validator';
export class CreateArticleDto {
@IsString()
@IsNotEmpty()
@MaxLength(200)
title: string;
@IsString()
@IsNotEmpty()
body: string;
}
Note: with ESM, a TypeORM relation between two entity files needs the Relation<User> wrapper type from typeorm on the property to avoid a circular-import error.
Step 3: Service and controller
// src/articles/articles.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Article } from './entities/article.entity.js';
import { CreateArticleDto } from './dto/create-article.dto.js';
@Injectable()
export class ArticlesService {
constructor(
@InjectRepository(Article)
private readonly repo: Repository<Article>,
) {}
findAll(page: number, limit: number) {
return this.repo.find({
skip: (page - 1) * limit,
take: limit,
order: { createdAt: 'DESC' },
});
}
async findOne(id: number) {
const article = await this.repo.findOne({ where: { id } });
if (!article) throw new NotFoundException(`Article #${id} not found`);
return article;
}
create(dto: CreateArticleDto) {
return this.repo.save(this.repo.create(dto));
}
async remove(id: number) {
const result = await this.repo.delete(id);
if (result.affected === 0) throw new NotFoundException(`Article #${id} not found`);
}
}
// src/articles/articles.controller.ts
import {
Controller, Get, Post, Body, Param, Delete,
ParseIntPipe, DefaultValuePipe, UseGuards, Query, HttpCode, HttpStatus,
} from '@nestjs/common';
import { ArticlesService } from './articles.service.js';
import { CreateArticleDto } from './dto/create-article.dto.js';
import { AuthGuard } from '../auth/auth.guard.js';
@Controller('articles')
export class ArticlesController {
constructor(private readonly articlesService: ArticlesService) {}
@Get()
findAll(
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
@Query('limit', new DefaultValuePipe(20), ParseIntPipe) limit: number,
) {
return this.articlesService.findAll(page, limit);
}
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
return this.articlesService.findOne(id);
}
@Post()
@UseGuards(AuthGuard)
create(@Body() dto: CreateArticleDto) {
return this.articlesService.create(dto);
}
@Delete(':id')
@UseGuards(AuthGuard)
@HttpCode(HttpStatus.NO_CONTENT)
remove(@Param('id', ParseIntPipe) id: number) {
return this.articlesService.remove(id);
}
}
Step 4: Auth module and guard
The guard needs JwtService, so the module that uses it must import a module that provides it. Put JwtModule in an AuthModule and export it; without that import Nest fails at startup with "can't resolve dependencies of the AuthGuard".
// src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtModule } from '@nestjs/jwt';
import { AuthGuard } from './auth.guard.js';
@Module({
imports: [
JwtModule.registerAsync({
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
secret: config.getOrThrow<string>('JWT_SECRET'),
signOptions: { expiresIn: '1h' },
}),
}),
],
providers: [AuthGuard],
exports: [JwtModule, AuthGuard],
})
export class AuthModule {}
// src/auth/auth.guard.ts
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import type { Request } from 'express';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private readonly jwtService: JwtService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest<Request & { user?: unknown }>();
const [type, token] = request.headers.authorization?.split(' ') ?? [];
if (type !== 'Bearer' || !token) throw new UnauthorizedException();
try {
request.user = await this.jwtService.verifyAsync(token);
return true;
} catch {
throw new UnauthorizedException();
}
}
}
Step 5: Feature module, root module, bootstrap
// src/articles/articles.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthModule } from '../auth/auth.module.js';
import { ArticlesController } from './articles.controller.js';
import { ArticlesService } from './articles.service.js';
import { Article } from './entities/article.entity.js';
@Module({
imports: [TypeOrmModule.forFeature([Article]), AuthModule],
controllers: [ArticlesController],
providers: [ArticlesService],
exports: [ArticlesService],
})
export class ArticlesModule {}
// src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ArticlesModule } from './articles/articles.module.js';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
TypeOrmModule.forRootAsync({
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
type: 'postgres',
host: config.get('DB_HOST', 'localhost'),
port: config.get<number>('DB_PORT', 5432),
database: config.get('DB_NAME', 'newsdesk'),
username: config.get('DB_USER', 'postgres'),
password: config.get('DB_PASSWORD'),
autoLoadEntities: true,
synchronize: config.get('NODE_ENV') !== 'production',
}),
}),
ArticlesModule,
],
})
export class AppModule {}
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module.js';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
app.enableCors({ origin: process.env.CORS_ORIGIN });
await app.listen(process.env.PORT ?? 3000);
}
await bootstrap();
Set JWT_SECRET and DB_PASSWORD in .env (never commit it); DB_HOST, DB_NAME, DB_USER, CORS_ORIGIN are optional.
Step 6: Test
Unit-test the service by providing a fake repository: Test.createTestingModule({ providers: [ArticlesService, { provide: getRepositoryToken(Article), useValue: { find: vi.fn().mockResolvedValue([]) } }] }).compile(), then moduleRef.get(ArticlesService). Run npm test (Vitest in ESM projects, Jest in CommonJS ones); npm run build compiles to dist/, npm run start:dev watches.
Examples
Example 1: "Add an articles API with login protection"
Follow Steps 1-5, start Postgres, set JWT_SECRET, run npm run start:dev. Then curl 'http://localhost:3000/articles?page=1&limit=5' returns a JSON array, and curl -X POST http://localhost:3000/articles -H 'Content-Type: application/json' -d '{"title":"Q3 report","body":"Revenue is up"}' answers 401 Unauthorized until a valid Authorization: Bearer <jwt> header is sent; with a valid token, a body with an empty title answers 400 with the validation messages.
Example 2: "Upgrade my NestJS 11 service to 12"
Install @nestjs/cli@latest and @nestjs/schematics@latest globally, run nest upgrade --dry-run to preview, then nest upgrade. It moves all @nestjs/* packages to 12 together, bumps TypeScript to 6 and applies the mechanical changes. A CommonJS app can stay CommonJS. If you keep Jest, run the tests on Node.js 24.9+ or move to Vitest, otherwise the ESM-only packages fail with ERR_REQUIRE_ASYNC_MODULE.
Guidelines
- One module per feature; import only the modules you need, and export a provider before another module can inject it.
- Use
ValidationPipewithwhitelist: trueto strip unknown properties. - Keep
synchronizeoff in production; use TypeORM migrations. - Read secrets through
ConfigService.getOrThrowso a missingJWT_SECRETstops startup instead of signing with an empty key. - Version 12 adds Standard Schema validation (Zod, Valibot):
@Body({ schema })plus a globalStandardSchemaValidationPipe;class-validatorremains supported. - Keep every
@nestjs/*package on the same major version.