Skip to content

NestJS with TypeScript — Enterprise Backend Architecture

DodaTech Updated 2026-06-28 8 min read

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

  1. 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.

  2. How do you validate request bodies in NestJS? Create a DTO class with class-validator decorators and use @Body(new ValidationPipe()) in the controller.

  3. What's the difference between a Guard and an Interceptor? Guards decide whether a request proceeds (authorization), Interceptors transform requests/responses (logging, caching, transformation).

  4. 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.

  5. 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

Is NestJS similar to Angular?

Yes. NestJS was inspired by Angular's architecture — modules, decorators, dependency injection, and the CLI pattern. Angular developers feel right at home.

Can I use Express middleware with NestJS?

Yes. NestJS runs on top of Express by default. Use app.use() in main.ts for Express middleware, or wrap it in a NestJS middleware class.

Does NestJS support GraphQL?

Yes. NestJS has first-class Graphql support with code-first and schema-first approaches, built-in resolvers, and subscription handling.

How does NestJS testing work?

NestJS provides testing utilities to create isolated modules. Use Test.createTestingModule() to mock dependencies and test controllers and services independently.

Is NestJS suitable for microservices?

Yes. NestJS has built-in microservice support with transports (TCP, Redis, RabbitMQ, Kafka) and patterns like event-driven communication.

Should I use NestJS or plain Express?

NestJS for large applications with multiple teams. Express for small APIs or when you prefer minimal framework overhead.

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