@nestjs/core
NestJS is a full server-side framework for Node.js, built in TypeScript, that gives your backend an enforced architecture: modules, dependency injection, controllers with decorators, and a layered request pipeline of guards, pipes, interceptors, and exception filters. The design is openly inspired by Angular. Under the hood it runs Express by default (Express 5 as of Nest 11) or Fastify via an adapter, so the HTTP layer is swappable. @nestjs/core is the runtime container; you always pair it with @nestjs/common and a platform adapter.
The strongest choice in Node for large, long-lived backends built by teams, because the structure it imposes is the product. For small services the same structure is mostly paperwork; reach for something lighter.
Use it if
- You have a team of more than a few backend developers and want every feature to follow the same module/controller/service shape without arguing about it
- You are building a large API that will grow into GraphQL, WebSockets, microservices, or queues: Nest has official first-party packages for each behind one DI system
- You come from Angular, Spring, or .NET and want constructor injection, decorators, and testable units instead of hand-wired Express middleware
- You need structured testing: @nestjs/testing lets you swap any provider with a mock through the DI container instead of monkey-patching imports
- You are building a small API or a handful of endpoints: the per-feature ceremony (module + controller + service + DTOs) is real overhead that Hono or Fastify skip entirely
- You deploy to serverless or edge runtimes: cold starts pay for the DI container bootstrap, and the framework assumes a long-lived Node process
- Your team dislikes decorators and DI magic: Nest leans on experimentalDecorators and emitDecoratorMetadata, which means tsc or SWC, and plain esbuild pipelines strip the metadata DI depends on
- You want to learn 'just Node': Nest abstracts Express so thoroughly that debugging often means understanding both the framework's request lifecycle and the platform underneath it
Setup reality
The blessed path is npm i -g @nestjs/cli && nest new project, which scaffolds tsconfig, ESLint, Jest, and the app skeleton for you; doing it by hand means installing @nestjs/common, @nestjs/core, @nestjs/platform-express, rxjs, and reflect-metadata (all peer-coupled to the same major), then enabling experimentalDecorators and emitDecoratorMetadata in tsconfig. Validation needs two more packages (class-validator, class-transformer) that nothing installs for you. Expect npm peer-dependency warnings around optional packages like @nestjs/websockets and @nestjs/microservices even when you do not use them, and expect major-version upgrades (10 to 11 moved to Express 5 and path-to-regexp 8) to change wildcard route syntax.
Patterns
Bootstrap the application (main.ts)bootstrap-app
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.enableCors();
app.setGlobalPrefix('api');
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();import 'reflect-metadata' must run before anything else; the CLI scaffold handles it, hand-rolled setups forget it and DI fails cryptically.
Declare a feature moduledefine-module
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
@Module({
controllers: [CatsController],
providers: [CatsService],
exports: [CatsService], // visible to modules that import CatsModule
})
export class CatsModule {}A provider is private to its module unless listed in exports; the 'Nest can't resolve dependencies' error is almost always a missing import/export.
Controller with route params and status codescontroller-route
import { Controller, Get, Post, Param, Body, HttpCode } from '@nestjs/common';
@Controller('cats')
export class CatsController {
constructor(private readonly catsService: CatsService) {}
@Get(':id')
findOne(@Param('id') id: string) {
return this.catsService.findOne(id);
}
@Post()
@HttpCode(201)
create(@Body() dto: CreateCatDto) {
return this.catsService.create(dto);
}
}Return a value and Nest serializes it to JSON; inject @Res() and you take over the response entirely, silently disabling interceptors for that handler.
Injectable service via constructor injectioninjectable-service
import { Injectable, NotFoundException } from '@nestjs/common';
@Injectable()
export class CatsService {
private readonly cats: Cat[] = [];
findOne(id: string): Cat {
const cat = this.cats.find((c) => c.id === id);
if (!cat) throw new NotFoundException(`Cat ${id} not found`);
return cat;
}
}Built-in HttpException subclasses (NotFoundException etc.) become proper HTTP responses automatically; plain Errors become opaque 500s.
Validate request bodies with DTOsdto-validation
// npm i class-validator class-transformer
import { IsString, IsInt, Min } from 'class-validator';
export class CreateCatDto {
@IsString() name: string;
@IsInt() @Min(0) age: number;
}
// main.ts
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // strip unknown properties
transform: true, // convert primitives to DTO types
}));Without whitelist: true, unknown JSON fields pass straight through to your service layer; class-validator and class-transformer are separate installs.
Protect routes with a guardauth-guard
import { CanActivate, ExecutionContext, Injectable, UseGuards } from '@nestjs/common';
@Injectable()
export class ApiKeyGuard implements CanActivate {
canActivate(ctx: ExecutionContext): boolean {
const req = ctx.switchToHttp().getRequest();
return req.headers['x-api-key'] === process.env.API_KEY;
}
}
@UseGuards(ApiKeyGuard)
@Controller('admin')
export class AdminController {}Guards run before pipes and interceptors; returning false yields a 403, so throw UnauthorizedException yourself if you want a 401.
Load environment config the Nest wayconfig-env
// npm i @nestjs/config
import { ConfigModule, ConfigService } from '@nestjs/config';
@Module({
imports: [ConfigModule.forRoot({ isGlobal: true })],
})
export class AppModule {}
// anywhere via DI:
constructor(private config: ConfigService) {}
this.config.get<string>('DATABASE_URL');@nestjs/config wraps dotenv; isGlobal: true saves you importing ConfigModule into every feature module.
Shape error responses with an exception filterexception-filter
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
@Catch(HttpException)
export class HttpErrorFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const res = host.switchToHttp().getResponse();
res.status(exception.getStatus()).json({
statusCode: exception.getStatus(),
message: exception.message,
timestamp: new Date().toISOString(),
});
}
}
// main.ts: app.useGlobalFilters(new HttpErrorFilter());Globally registered filters instantiated with new cannot use DI; register them as APP_FILTER providers if they need injected dependencies.
Swap Express for Fastifyfastify-adapter
// npm i @nestjs/platform-fastify
import { NestFactory } from '@nestjs/core';
import {
FastifyAdapter,
NestFastifyApplication,
} from '@nestjs/platform-fastify';
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter(),
);
await app.listen(3000, '0.0.0.0');Fastify is faster but Express-specific middleware and libraries expecting req/res from Express will not work; listen needs the 0.0.0.0 host in containers.
Unit test with the DI containerunit-testing
import { Test } from '@nestjs/testing';
const moduleRef = await Test.createTestingModule({
controllers: [CatsController],
providers: [CatsService],
})
.overrideProvider(CatsService)
.useValue({ findOne: jest.fn().mockReturnValue({ id: '1' }) })
.compile();
const controller = moduleRef.get(CatsController);
expect(controller.findOne('1')).toEqual({ id: '1' });overrideProvider swaps real services for mocks without touching module code; for HTTP-level tests add supertest against moduleRef.createNestApplication().
Graceful shutdown hookslifecycle-shutdown
import { Injectable, OnModuleDestroy } from '@nestjs/common';
@Injectable()
export class DbService implements OnModuleDestroy {
async onModuleDestroy() {
await this.pool.end();
}
}
// main.ts
app.enableShutdownHooks(); // required for SIGTERM handlingShutdown hooks are off by default because they cost a signal listener; without enableShutdownHooks() your onModuleDestroy never fires on SIGTERM in Kubernetes.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| fastify | npm | You want speed and schema-based validation without a framework dictating your architecture |
| hono | npm | Small services or edge/serverless deploys where startup weight and runtime portability matter |
| express | npm | You want the minimal, everyone-knows-it baseline and are happy to pick your own structure |