- Project Setup (from zero to running)
1.1 Install Nest CLI & create app
1.2 Add TypeORM + Postgres
1.3 Global validation pipes (class-validator)
1.4 Multiple environments (
.env
per env) + validation 1.5 API Docs: Swagger (OpenAPI) 1.6 Architecture Docs: Compodoc 1.7 Recommended scripts - How-to by Use Case 2.1 Create a new module quickly 2.2 When a circular dependency appears 2.3 Add a new entity (create → import → use) 2.4 Create and run database migrations (TypeORM 0.3+)
- Controller / Service / Repository patterns
- Guards, Pipes, Interceptors, Filters (quick ref)
- Caching, Scheduling, Events
- Common Confusions (fast clarifications)
- Best Practices (production-minded)
- Troubleshooting (frequent gotchas)
- Contributing
npm i -g @nestjs/cli
nest new my-app
cd my-app
npm run start:dev
npm i @nestjs/typeorm typeorm pg
src/app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'postgres',
url: process.env.DATABASE_URL, // e.g. postgres://user:pass@localhost:5432/db
autoLoadEntities: true,
synchronize: false, // NEVER true in prod. Use migrations.
logging: process.env.NODE_ENV !== 'production',
}),
],
})
export class AppModule {}
npm i class-validator class-transformer
src/main.ts
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // strip unknown props
forbidNonWhitelisted: true, // throw if unknown props present
transform: true, // auto-transform payloads to DTO types
}));
await app.listen(process.env.PORT || 3000);
}
bootstrap();
npm i @nestjs/config joi
Project root:
.env
.env.development
.env.staging
.env.production
src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import * as Joi from 'joi';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: [
`.env.${process.env.NODE_ENV || 'development'}`,
'.env', // fallback
],
validationSchema: Joi.object({
NODE_ENV: Joi.string().valid('development','test','staging','production').required(),
DATABASE_URL: Joi.string().uri().required(),
PORT: Joi.number().default(3000),
}),
}),
],
})
export class AppModule {}
Usage in services:
import { ConfigService } from '@nestjs/config';
constructor(private readonly config: ConfigService) {}
const dbUrl = this.config.get<string>('DATABASE_URL');
npm i @nestjs/swagger swagger-ui-express
src/main.ts
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
// ... after creating the app
const config = new DocumentBuilder()
.setTitle('My API')
.setDescription('REST API for My App')
.setVersion('1.0.0')
.addBearerAuth() // if JWT
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('docs', app, document); // http://localhost:3000/docs
Decorate DTOs & endpoints:
import { ApiProperty, ApiTags, ApiOkResponse } from '@nestjs/swagger';
@ApiTags('users')
@Controller('users')
export class UsersController {
@ApiOkResponse({ description: 'List users' })
@Get() findAll() {}
}
export class CreateUserDto {
@ApiProperty({ example: 'alice@example.com' })
email: string;
}
npm i -D @compodoc/compodoc
Serve docs:
npx compodoc -p tsconfig.json -s -r 8081
# open http://localhost:8081
Export static site:
npx compodoc -p tsconfig.json -d ./compodoc
package.json
{
"scripts": {
"start": "nest start",
"start:dev": "nest start --watch",
"build": "nest build",
"lint": "eslint .",
"test": "jest",
"test:e2e": "jest --config ./test/jest-e2e.json",
"docs:openapi": "node ./scripts/export-openapi.js",
"docs:compodoc": "compodoc -p tsconfig.json -s -r 8081"
}
}
nest g module users
nest g service users --flat --no-spec
nest g controller users --no-spec
Wire it
// users.module.ts
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
@Module({
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService], // export if other modules need it
})
export class UsersModule {}
Consume from another module
// orders.module.ts
import { Module } from '@nestjs/common';
import { UsersModule } from '../users/users.module';
import { OrdersService } from './orders.service';
@Module({ imports: [UsersModule], providers: [OrdersService] })
export class OrdersModule {}
Symptoms: Nest can't resolve dependencies of X (Y, ?)... argument Z at index 1 is available...
Fix with forwardRef
on BOTH sides that reference each other:
// users.module.ts
import { Module, forwardRef } from '@nestjs/common';
import { OrdersModule } from '../orders/orders.module';
import { UsersService } from './users.service';
@Module({
imports: [forwardRef(() => OrdersModule)],
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}
// orders.module.ts
import { Module, forwardRef } from '@nestjs/common';
import { UsersModule } from '../users/users.module';
import { OrdersService } from './orders.service';
@Module({
imports: [forwardRef(() => UsersModule)],
providers: [OrdersService],
exports: [OrdersService],
})
export class OrdersModule {}
Inject using forwardRef
:
import { Inject, forwardRef } from '@nestjs/common';
constructor(@Inject(forwardRef(() => OrdersService)) private orders: OrdersService) {}
Prefer refactoring to break cycles (extract a shared “core” provider) when possible.
1) Define entity
// src/users/user.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, Index } from 'typeorm';
@Entity('users')
export class UserEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index({ unique: true })
@Column({ nullable: true }) // Postgres allows multiple NULLs with unique index
email?: string;
@Column()
name: string;
}
2) Register in module
// src/users/users.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import { UserEntity } from './user.entity';
@Module({
imports: [TypeOrmModule.forFeature([UserEntity])],
providers: [UsersService],
controllers: [UsersController],
exports: [UsersService],
})
export class UsersModule {}
3) Use repository in service
// src/users/users.service.ts
import { Injectable } from '@nestjs/common';
import { Repository } from 'typeorm';
import { UserEntity } from './user.entity';
import { InjectRepository } from '@nestjs/typeorm';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(UserEntity) private readonly users: Repository<UserEntity>,
) {}
create(data: Partial<UserEntity>) {
return this.users.save(this.users.create(data));
}
findAll() {
return this.users.find();
}
}
1) Dedicated data source for CLI
// src/database/data-source.ts
import { DataSource } from 'typeorm';
import { UserEntity } from '../users/user.entity';
export const AppDataSource = new DataSource({
type: 'postgres',
url: process.env.DATABASE_URL,
entities: [UserEntity],
migrations: [__dirname + '/migrations/*.{ts,js}'],
synchronize: false,
logging: false,
});
export default AppDataSource;
2) Scripts (TypeScript execution)
npm i -D ts-node
package.json
{
"scripts": {
"typeorm": "node --loader ts-node/esm ./node_modules/typeorm/cli.js",
"db:gen": "npm run typeorm -- migration:generate src/database/migrations/Auto -d src/database/data-source.ts",
"db:create": "npm run typeorm -- migration:create src/database/migrations/Manual",
"db:run": "npm run typeorm -- migration:run -d src/database/data-source.ts",
"db:revert": "npm run typeorm -- migration:revert -d src/database/data-source.ts"
}
}
If using CommonJS, replace the loader with
ts-node/register
and set"module": "commonjs"
intsconfig.json
.
3) Generate after entity change
# Update or add entities first, then:
npm run db:gen
npm run db:run
4) Example migration (manual)
import { MigrationInterface, QueryRunner } from "typeorm";
export class CreateUsers1730000000000 implements MigrationInterface {
name = 'CreateUsers1730000000000'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
CREATE TABLE "users" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
"email" varchar UNIQUE,
"name" varchar NOT NULL
)
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE "users"`);
}
}
Controller — thin
import { Controller, Get, Post, Body } from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dtos/create-user.dto';
@Controller('users')
export class UsersController {
constructor(private readonly users: UsersService) {}
@Get()
findAll() { return this.users.findAll(); }
@Post()
create(@Body() dto: CreateUserDto) { return this.users.create(dto); }
}
Service — business logic
import { Injectable } from '@nestjs/common';
import { Repository } from 'typeorm';
import { InjectRepository } from '@nestjs/typeorm';
import { UserEntity } from './user.entity';
@Injectable()
export class UsersService {
constructor(@InjectRepository(UserEntity) private repo: Repository<UserEntity>) {}
// business logic here
}
DTOs + Validation
import { IsEmail, IsOptional, IsString } from 'class-validator';
export class CreateUserDto {
@IsOptional() @IsEmail() email?: string;
@IsString() name: string;
}
Guard (auth/perm)
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(ctx: ExecutionContext) {
const req = ctx.switchToHttp().getRequest();
return !!req.headers.authorization;
}
}
Pipe (validate/transform)
import { Injectable, PipeTransform, BadRequestException } from '@nestjs/common';
@Injectable()
export class ParseIdPipe implements PipeTransform {
transform(v: string) {
if (!/^[0-9a-f-]{36}$/i.test(v)) throw new BadRequestException('Invalid UUID');
return v;
}
}
Interceptor (wrap/transform/log)
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { tap } from 'rxjs/operators';
import { Observable } from 'rxjs';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(ctx: ExecutionContext, next: CallHandler): Observable<any> {
const started = Date.now();
return next.handle().pipe(tap(() => console.log('took', Date.now() - started, 'ms')));
}
}
Exception Filter (centralize errors)
import { Catch, ExceptionFilter, ArgumentsHost, HttpException } from '@nestjs/common';
@Catch(HttpException)
export class HttpErrorFilter implements ExceptionFilter {
catch(e: HttpException, host: ArgumentsHost) {
const res = host.switchToHttp().getResponse();
res.status(e.getStatus()).json({ error: e.message });
}
}
Cache
import { Module } from '@nestjs/common';
import { CacheModule, CacheInterceptor } from '@nestjs/cache-manager';
@Module({
imports: [CacheModule.register({ ttl: 60 })],
})
export class AppModule {}
import { UseInterceptors } from '@nestjs/common';
@UseInterceptors(CacheInterceptor)
@Get() getHeavy() { /* ... */ }
Scheduling
npm i @nestjs/schedule
import { Injectable } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
@Injectable()
export class TasksService {
@Cron(CronExpression.EVERY_30_SECONDS)
handleCron() { /* ... */ }
}
Events
npm i @nestjs/event-emitter
import { EventEmitter2 } from '@nestjs/event-emitter';
constructor(private emitter: EventEmitter2) {}
this.emitter.emit('user.created', { id: '...' });
-
Service vs Repository Service = business logic/orchestration. Repository = persistence (CRUD) injected into the service.
-
DTO vs Entity DTO = request/response contract + validation. Entity = DB mapping. Don’t return entities directly to clients.
-
Providers vs Exports
providers
are local to the module. To reuse elsewhere, add toexports
and import that module where needed. -
Middleware vs Interceptor Middleware runs before route handler at framework level (raw req/res). Interceptor wraps handler execution (transform/measure/cache).
-
Circular deps Use
forwardRef
as a stopgap. Prefer extracting shared logic into a separate module to break the cycle. -
unique
+nullable
(emails) Postgres allows multipleNULL
rows under a unique index. “Unique when present” is fine withnullable: true
+unique
.
- Disable
synchronize
in prod; use migrations. - Validate env with
@nestjs/config
+Joi
. - Use DTOs + global
ValidationPipe
withwhitelist
andtransform
. - Keep controllers slim; push logic into services.
- Organize by domain (users/, orders/, payments/) not by type.
- Put shared utilities in a
common
/shared
module (avoid circular deps). - Add Swagger early; export OpenAPI in CI for clients.
- Use Compodoc for onboarding/architecture visibility.
- Log structured JSON (Pino/Winston) in prod; add request-ID correlation.
- Cover unit + e2e tests; centralize exception filtering.
-
“Nest can’t resolve dependencies…” Missing
exports: [X]
or forgot to import the module that provides X, or a circular dep—useforwardRef
or refactor. -
Migrations don’t see entities Ensure the CLI data source includes correct
entities
andmigrations
paths; pass-d src/database/data-source.ts
. -
Validation not running
app.useGlobalPipes(new ValidationPipe(...))
inmain.ts
, and DTOs must be used in controller method params. -
Swagger not showing models Decorate DTO props with
@ApiProperty()
and tag controllers with@ApiTags()
. -
Config not loading Check
envFilePath
ordering andNODE_ENV
; ensurevalidationSchema
matches required vars.
Spotted a gap or want to improve examples? Read the Contributing Guide and open a PR.
Quick start: fork → branch → edit → pnpm test
(or npm run test
) → PR with a clear description.