NestJS with TypeScript — Enterprise Backend Architecture
In this tutorial, you will learn about NestJS with TypeScript. We cover key concepts, practical examples, and best practices to help you master this topic.
NestJS brings a structured module-based architecture to TypeScript backend development, using decorators and dependency injection to organize enterprise applications in a way that scales across teams and features.
What You'll Learn
- NestJS project setup and CLI
- Modules, Controllers, and Services
- Dependency Injection in NestJS
- Pipes, Guards, and Interceptors
- Database integration with TypeORM
- Testing NestJS applications
Why It Matters
Express.js applications often devolve into unstructured route files as they grow. NestJS enforces a modular architecture from day one, making it easy to split features across teams, swap implementations, and maintain type safety across the entire application.
Real-World Use
The Durga Antivirus Pro backend uses NestJS for its customer management dashboard. Each feature (licenses, devices, payments, support tickets) lives in its own module with clear boundaries — enabling five teams to work simultaneously without merge conflicts.
Learning Path
flowchart LR A[Express APIs] --> B[NestJS] B --> C[Database Access] B --> D[You Are Here] C --> E[Testing] D --> F[SOLID Principles]
Setting Up a NestJS Project
NestJS has its own CLI for scaffolding:
npm install -g @nestjs/cli
nest new my-app --package-manager npm
cd my-app
The generated project includes a src directory with a main module, controller, and service:
src/
app.controller.ts
app.controller.spec.ts
app.module.ts
app.service.ts
main.ts
Basic Module Structure
// src/main.ts — application entry point
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
console.log('NestJS running on http://localhost:3000');
}
bootstrap();
// src/app.module.ts — root module
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
Controllers — Request Handlers
Controllers handle incoming HTTP requests. NestJS decorators map routes and extract typed parameters:
// src/users/users.controller.ts
import { Controller, Get, Post, Param, Body, Query } from '@nestjs/common';
interface CreateUserDto {
name: string;
email: string;
}
interface User {
id: string;
name: string;
email: string;
}
@Controller('users')
export class UsersController {
private users: User[] = [];
@Get()
findAll(@Query('page') page?: string): User[] {
console.log(`Fetching page: ${page ?? '1'}`);
return this.users;
}
@Get(':id')
findOne(@Param('id') id: string): User {
const user = this.users.find((u) => u.id === id);
if (!user) {
throw new Error('User not found');
}
return user;
}
@Post()
create(@Body() createUserDto: CreateUserDto): User {
const user: User = {
id: String(this.users.length + 1),
...createUserDto,
};
this.users.push(user);
return user;
}
}
The @Controller('users') decorator sets the route prefix. All methods in this class are mounted under /users. The @Param, @Body, and @Query decorators extract typed values from the request.
Services — Business Logic
Services contain business logic and are injected into controllers:
// src/users/users.service.ts
import { Injectable } from '@nestjs/common';
export interface User {
id: string;
name: string;
email: string;
}
export interface CreateUserInput {
name: string;
email: string;
}
@Injectable()
export class UsersService {
private users: User[] = [];
findAll(): User[] {
return this.users;
}
findOne(id: string): User | undefined {
return this.users.find((u) => u.id === id);
}
create(input: CreateUserInput): User {
const user: User = {
id: String(this.users.length + 1),
...input,
};
this.users.push(user);
return user;
}
}
The @Injectable() decorator marks the class as a provider that NestJS can inject into other classes.
Wiring Controller and Service
// src/users/users.module.ts
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
@Module({
controllers: [UsersController],
providers: [UsersService],
})
export class UsersModule {}
// src/app.module.ts
import { Module } from '@nestjs/common';
import { UsersModule } from './users/users.module';
@Module({
imports: [UsersModule],
})
export class AppModule {}
Dependency Injection
NestJS uses constructor-based dependency injection. The injector resolves dependencies automatically:
// src/users/users.controller.ts
import { Controller, Get } from '@nestjs/common';
import { UsersService, User } from './users.service';
@Controller('users')
export class UsersController {
// NestJS injects UsersService automatically
constructor(private readonly usersService: UsersService) {}
@Get()
findAll(): User[] {
return this.usersService.findAll();
}
}
How it works: NestJS scans the constructor parameters, looks up matching providers in the module, and creates the dependency. If the provider depends on other providers, NestJS resolves the entire chain recursively.
Pipes — Validation and Transformation
Pipes transform or validate input data. NestJS ships with built-in validation using class-validator:
npm install class-validator class-transformer
// src/users/dto/create-user.dto.ts
import { IsString, IsEmail, MinLength, MaxLength } from 'class-validator';
export class CreateUserDto {
@IsString()
@MinLength(2)
@MaxLength(50)
name: string;
@IsEmail()
email: string;
}
// src/users/users.controller.ts
import { Controller, Post, Body, ValidationPipe } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
@Controller('users')
export class UsersController {
@Post()
create(@Body(new ValidationPipe()) createUserDto: CreateUserDto) {
// If validation fails, NestJS returns 400 Bad Request automatically
return this.usersService.create(createUserDto);
}
}
Guards — Authorization
Guards determine whether a request should proceed. They can be attached to specific routes or entire controllers:
// src/auth/roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable()
export class RolesGuard implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest();
// Check if user has required role
return request.headers['x-api-key'] === 'secret-key';
}
}
// src/users/users.controller.ts
import { Controller, Get, UseGuards } from '@nestjs/common';
import { RolesGuard } from '../auth/roles.guard';
@Controller('admin')
export class AdminController {
@UseGuards(RolesGuard)
@Get('dashboard')
getDashboard(): string {
return 'Protected dashboard data';
}
}
Interceptors — Transform Responses
Interceptors modify responses or add cross-cutting behavior:
// src/common/logging.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable, tap } from 'rxjs';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const now = Date.now();
return next.handle().pipe(
tap(() => {
console.log(`${request.method} ${request.url} — ${Date.now() - now}ms`);
}),
);
}
}
Common Mistakes
1. Forgetting to register providers in the module
If a service isn't listed in providers of a module, NestJS cannot inject it. The error message is cryptic: Nest can't resolve dependencies.
2. Circular dependencies between modules
If Module A imports Module B and Module B imports Module A, NestJS throws a circular dependency error. Use forwardRef() to resolve.
3. Not using DTOs for validation
Raw @Body() without a validation pipe lets any data through. Always define DTOs with class-validator decorators.
4. Using any for controller parameters
NestJS types the return values of @Param() and @Query() — use specific types instead of any.
5. Mixing provider scopes incorrectly
NestJS providers are singletons by default. Use @Injectable({ scope: Scope.REQUEST }) for request-scoped providers carefully.
6. Over-decorating classes
Too many decorators on a single controller method makes it hard to read. Extract cross-cutting concerns into interceptors and guards.
7. Not using the CLI for code generation
NestJS CLI has generators: nest g module users, nest g service users. Using them ensures consistent structure.
Practice Questions
What does the
@Injectable()decorator do? It marks a class as a provider that NestJS can inject into other classes through constructor-based dependency injection.How do you validate request bodies in NestJS? Create a DTO class with
class-validatordecorators and use@Body(new ValidationPipe())in the controller.What's the difference between a Guard and an Interceptor? Guards decide whether a request proceeds (authorization), Interceptors transform requests/responses (logging, caching, transformation).
How do you organize code into feature modules? Each feature gets its own module (e.g.,
UsersModule) with its own controller, service, and DTOs. Import it into the root module.What happens when NestJS can't resolve a dependency? It throws a runtime error at startup with a message like
Nest can't resolve dependencies of UsersController. Missing providers and circular dependencies are the most common causes.
Challenge
Build a NestJS application with three modules (Users, Products, Orders) where each module has its own controller, service, and DTOs. Add validation, a guard for admin routes, and an interceptor for request logging.
FAQ
Mini Project
Build a NestJS blog platform:
- User module: registration, login, profile
- Post module: CRUD for blog posts with validation
- Comment module: nested routes under posts
- Authentication guard: protect write routes
- Logging interceptor: log all requests
- Validation pipe: validate all DTOs
Use the NestJS CLI to generate modules and services.
What's Next
You've built enterprise-grade backends with NestJS and TypeScript. Now learn how to connect your NestJS app to a real database with {{< ref "47-database-access" >}}, or write tests for your application with {{< ref "48-testing" >}}.
For architectural patterns, see {{< ref "50-solid-principles" >}}.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro