Skip to main content
NestJS Architecture

Chapter 1: NestJS Fundamentals

NestJS is a progressive Node.js framework for building efficient, reliable, and scalable server-side applications. Think of it as the “Angular for the backend”—it brings structure, modularity, and best practices to Node.js development, making it ideal for both startups and enterprises.

1.1 Introduction to NestJS

NestJS provides a robust foundation for building enterprise-grade applications. It leverages TypeScript by default, supports modular architecture, and encourages best practices such as dependency injection, testability, and separation of concerns.

Why NestJS?

TypeScript-first approach:
  • TypeScript provides compile-time type checking, catching errors before runtime
  • Better IDE support with autocomplete and refactoring tools
  • Self-documenting code through type annotations
  • Gradual adoption—you can use JavaScript, but TypeScript is recommended
Modular and testable architecture:
  • Code is organized into logical modules, making it easy to scale and maintain
  • Each module encapsulates related functionality (controllers, services, providers)
  • Clear separation of concerns makes testing straightforward
  • Modules can be easily swapped or replaced without affecting the entire application
Dependency Injection (DI) system:
  • Write loosely coupled, easily testable code
  • Dependencies are injected rather than hard-coded, enabling better unit testing
  • Supports different provider scopes (singleton, request-scoped, transient)
  • Makes it easy to mock dependencies in tests
Versatile and extensible:
  • Build REST APIs, GraphQL endpoints, WebSockets, and microservices—all with the same framework
  • Extensive ecosystem with official and community packages
  • Integrates seamlessly with popular libraries (Express, Fastify, TypeORM, Prisma, etc.)
  • Supports multiple transport layers for microservices (TCP, Redis, RabbitMQ, NATS, gRPC)
Familiarity for Angular developers:
  • Similar decorators, modules, and dependency injection patterns
  • If you know Angular, NestJS will feel natural
  • Shared concepts reduce learning curve
Enterprise-ready features:
  • Built-in support for validation, transformation, and serialization
  • Exception filters for consistent error handling
  • Guards for authentication and authorization
  • Interceptors for cross-cutting concerns (logging, caching, transformation)
  • Pipes for data transformation and validation
Analogy:
Imagine building with Lego blocks—each block (module, controller, service) has a clear purpose and fits together to create a robust structure. NestJS gives you the blueprint and the blocks, ensuring everything fits perfectly.

When to Use NestJS

Perfect for:
  • Enterprise applications requiring structure and maintainability
  • Large teams where consistency matters
  • Applications that need to scale horizontally
  • Projects requiring extensive testing
  • Microservices architectures
  • Applications with complex business logic
Consider alternatives when:
  • Building simple CRUD APIs (Express might be simpler)
  • Prototyping quickly (though NestJS CLI helps here)
  • Team has no TypeScript experience (learning curve)

1.2 Setting Up Your Environment

Before you begin, ensure you have the necessary tools installed. NestJS requires Node.js and a package manager.

Prerequisites

Node.js:
  • Minimum version: Node.js 16.x or higher
  • Recommended: Node.js 18.x LTS or 20.x LTS
  • Check your version: node -v
Package Manager:
  • npm (comes with Node.js)
  • yarn (alternative, faster)
  • pnpm (alternative, disk-efficient)
TypeScript (optional but recommended):
  • NestJS CLI will set this up for you
  • Understanding TypeScript basics helps significantly

Installing the NestJS CLI

The NestJS CLI is a powerful tool that generates boilerplate code, helping you maintain consistency and save time.
npm install -g @nestjs/cli
Global vs Local Installation: Installing globally allows you to use nest command anywhere. Alternatively, you can use npx @nestjs/cli without global installation, or install it locally in your project.
Verify the installation:
nest --version
You should see the version number (e.g., 10.0.0).

Creating Your First Project

Let’s create a simple NestJS application to understand the structure:
nest new hello-nest
The CLI will prompt you to choose a package manager:
  • npm
  • yarn
  • pnpm
Package Manager Choice: Choose based on your preference. npm is the default and most widely used. yarn and pnpm offer faster installs and better disk efficiency.
After the CLI finishes, navigate to your project:
cd hello-nest
npm run start:dev
Open http://localhost:3000 in your browser. You should see: Hello World!
Development Mode: start:dev runs the app in watch mode, automatically restarting when you make changes. This is perfect for development. For production, use start:prod which builds and runs the optimized version.

Understanding the Project Structure

Let’s examine what the CLI generated:
hello-nest/
├── src/                    # Source code directory
│   ├── app.controller.ts   # Controller (handles HTTP requests)
│   ├── app.module.ts       # Root module (organizes the app)
│   ├── app.service.ts      # Service (business logic)
│   └── main.ts             # Entry point (bootstraps the app)
├── test/                   # Test files
│   ├── app.e2e-spec.ts     # End-to-end tests
│   └── jest-e2e.json       # Jest configuration for E2E tests
├── .eslintrc.js            # ESLint configuration
├── .gitignore              # Git ignore rules
├── .prettierrc             # Prettier configuration
├── nest-cli.json            # NestJS CLI configuration
├── package.json            # Dependencies and scripts
├── tsconfig.json           # TypeScript configuration
├── tsconfig.build.json     # TypeScript build configuration
└── README.md               # Project documentation
Key Files Explained: main.ts - Application Entry Point:
  • Bootstraps the NestJS application
  • Creates the NestFactory instance
  • Configures global pipes, filters, guards, interceptors
  • Starts the HTTP server
  • This is where your app “starts”
app.module.ts - Root Module:
  • The root module of your application
  • Imports other modules
  • Declares controllers and providers
  • Every NestJS app has at least one module (the root module)
app.controller.ts - Controller:
  • Handles incoming HTTP requests
  • Defines routes and HTTP methods
  • Delegates business logic to services
  • Returns responses to clients
app.service.ts - Service:
  • Contains business logic
  • Can be injected into controllers or other services
  • Should be stateless (no instance variables that change)
  • Handles data processing, validation, and orchestration
Understanding the Request Flow:
HTTP Request

main.ts (server starts)

app.module.ts (module loaded)

app.controller.ts (route matched)

app.service.ts (business logic executed)

Response sent back

1.3 Understanding the Core Concepts

NestJS is built around a few core building blocks. Understanding these concepts is crucial for building effective NestJS applications.

1.3.1 Modules

Modules are like folders in your computer—they organize related files (controllers, services) together. Every app has at least one root module (AppModule). Why Modules Matter:
  • Organization: Group related functionality together
  • Encapsulation: Control what’s exposed to other modules
  • Reusability: Modules can be imported and reused
  • Testing: Test modules in isolation
Basic Module Structure:
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';

@Module({
  controllers: [UsersController],  // Controllers that belong to this module
  providers: [UsersService],       // Services/providers available in this module
  exports: [UsersService],          // What this module exports (makes available to other modules)
})
export class UsersModule {}
Module Metadata Explained:
  • controllers: Array of controllers that handle HTTP requests for this module
  • providers: Services, repositories, factories, or values that can be injected
  • imports: Other modules whose exported providers you want to use
  • exports: Providers from this module that should be available to other modules
Best Practice: Keep modules focused and cohesive. Use feature modules to separate domains (e.g., UsersModule, AuthModule, OrdersModule). Example: Feature Module Structure:
// users/users.module.ts
@Module({
  controllers: [UsersController],
  providers: [UsersService, UserRepository],
  exports: [UsersService],  // Export so other modules can use UsersService
})
export class UsersModule {}

// app.module.ts
@Module({
  imports: [UsersModule],  // Import UsersModule to use its exported providers
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

1.3.2 Controllers

Controllers are like receptionists—they receive requests, ask the right service for help, and send back a response. They define routes and delegate business logic to services. Controller Responsibilities:
  • Define routes (URLs and HTTP methods)
  • Extract data from requests (params, query, body, headers)
  • Validate input (using DTOs and validation pipes)
  • Call services to handle business logic
  • Return responses (JSON, status codes)
  • Handle errors (throw exceptions)
Basic Controller Example:
import { Controller, Get, Post, Body, Param } from '@nestjs/common';
import { UsersService } from './users.service';

@Controller('users')  // Base route: /users
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Get()  // GET /users
  findAll() {
    return this.usersService.findAll();
  }

  @Get(':id')  // GET /users/:id
  findOne(@Param('id') id: string) {
    return this.usersService.findOne(id);
  }

  @Post()  // POST /users
  create(@Body() createUserDto: CreateUserDto) {
    return this.usersService.create(createUserDto);
  }
}
Key Decorators:
  • @Controller('route') - Defines the base route for all methods in this controller
  • @Get(), @Post(), @Put(), @Delete(), @Patch() - HTTP method decorators
  • @Param('name') - Extract route parameter
  • @Query('name') - Extract query parameter
  • @Body() - Extract request body
  • @Headers('name') - Extract header value
Tip: Controllers should be thin—move business logic to services. Controllers should only handle HTTP concerns (routing, request/response).

1.3.3 Providers & Services

Providers are classes that can be injected as dependencies. Services are a common type of provider, used for business logic, data access, etc. What Makes a Provider:
  • Any class decorated with @Injectable()
  • Can be injected into controllers or other providers
  • Managed by NestJS dependency injection container
  • Can have different scopes (singleton, request-scoped, transient)
Service Example:
import { Injectable } from '@nestjs/common';

@Injectable()  // Makes this class injectable
export class UsersService {
  private users = [
    { id: 1, name: 'Alice', email: '[email protected]' },
    { id: 2, name: 'Bob', email: '[email protected]' },
  ];

  findAll() {
    return this.users;
  }

  findOne(id: number) {
    return this.users.find(user => user.id === id);
  }

  create(userData: { name: string; email: string }) {
    const newUser = {
      id: this.users.length + 1,
      ...userData,
    };
    this.users.push(newUser);
    return newUser;
  }
}
Service Best Practices:
  • Keep services focused on a single responsibility
  • Make services stateless when possible (no mutable instance variables)
  • Use services for business logic, not HTTP concerns
  • Services can depend on other services (inject them in constructor)
Diagram: NestJS Request Flow
Client Request

Controller (receives request, extracts data)

Service (business logic, data processing)

Repository/Database (data persistence)

Service (processes data)

Controller (formats response)

Client Response
Analogy:
Think of a restaurant: the client is the customer, the controller is the waiter (takes orders, serves food), the service is the chef (prepares the meal), and the repository/database is the pantry (stores ingredients).

1.4 How Decorators Work Under the Hood

NestJS relies heavily on TypeScript decorators. Understanding how they work makes you a better NestJS developer.

What Are Decorators?

Decorators are special functions that can modify classes, methods, properties, or parameters. They’re prefixed with @ and executed at class definition time.
@Controller('users')     // Class decorator
export class UsersController {
  @Get(':id')            // Method decorator
  findOne(
    @Param('id') id: string  // Parameter decorator
  ) {}
}

How Decorators Work

Under the hood, decorators are just functions that receive metadata about the decorated target:
// A simple decorator is just a function
function Log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  
  descriptor.value = function (...args: any[]) {
    console.log(`Calling ${propertyKey} with:`, args);
    return originalMethod.apply(this, args);
  };
}

class Example {
  @Log
  sayHello(name: string) {
    return `Hello, ${name}!`;
  }
}

Reflect Metadata: The Magic Behind DI

NestJS uses the reflect-metadata library to store and retrieve metadata about classes. This is how dependency injection works!
// When you write this:
@Injectable()
export class UsersService {
  constructor(private db: DatabaseService) {}
}

// TypeScript compiles it to something like:
Reflect.defineMetadata('design:paramtypes', [DatabaseService], UsersService);
The DI Resolution Process:
1. NestJS sees @Injectable() decorator

2. Reflect.getMetadata('design:paramtypes', UsersService)

3. Returns: [DatabaseService]

4. NestJS creates DatabaseService instance (or gets from container)

5. NestJS creates UsersService with DatabaseService injected
Important: For this to work, you must enable emitDecoratorMetadata: true in your tsconfig.json. The NestJS CLI does this automatically.

NestJS Built-in Decorators

DecoratorTypePurpose
@Module()ClassDefines a module
@Controller()ClassDefines a controller
@Injectable()ClassMarks class as injectable
@Get(), @Post() etcMethodMaps HTTP methods
@Body(), @Param(), @Query()ParameterExtracts request data
@UseGuards()Class/MethodApplies guards
@UseInterceptors()Class/MethodApplies interceptors
@UsePipes()Class/MethodApplies pipes

Creating Custom Decorators

You can create your own decorators to reduce boilerplate:
import { createParamDecorator, ExecutionContext } from '@nestjs/common';

// Custom parameter decorator to get current user
export const CurrentUser = createParamDecorator(
  (data: unknown, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    return request.user;
  },
);

// Usage
@Get('profile')
getProfile(@CurrentUser() user: User) {
  return user;
}
Why This Matters: Understanding decorators helps you debug issues, create custom decorators, and understand NestJS internals. When something doesn’t work, check if the decorator is applied correctly and if metadata is being emitted.

1.5 Application Lifecycle

Understanding how a NestJS application starts and runs helps you configure it properly and debug issues.

Bootstrap Process

NestJS applications are initialized via the main.ts file. This is the entry point that bootstraps the root module and starts the HTTP server. Basic Bootstrap:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}
bootstrap();
What Happens During Bootstrap:
  1. NestFactory.create() - Creates a NestJS application instance
    • Loads the root module (AppModule)
    • Resolves all dependencies
    • Initializes all providers
    • Registers all controllers
  2. Module Loading - NestJS processes the module tree
    • Starts with the root module
    • Recursively loads imported modules
    • Resolves provider dependencies
    • Creates provider instances (based on scope)
  3. Controller Registration - All controllers are registered
    • Routes are mapped to controller methods
    • Middleware, guards, interceptors are attached
  4. Server Startup - HTTP server starts listening
    • Default port: 3000
    • Can be configured via environment variables

Enhanced Bootstrap with Configuration

In production, you’ll want to configure your app:
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  
  // Enable CORS
  app.enableCors();
  
  // Global validation pipe
  app.useGlobalPipes(new ValidationPipe({
    whitelist: true,        // Strip properties that don't have decorators
    forbidNonWhitelisted: true,  // Throw error if non-whitelisted properties exist
    transform: true,        // Automatically transform payloads to DTO instances
    transformOptions: {
      enableImplicitConversion: true,  // Enable implicit type conversion
    },
  }));
  
  // Global prefix for all routes
  app.setGlobalPrefix('api');
  
  const port = process.env.PORT || 3000;
  await app.listen(port);
  
  console.log(`Application is running on: http://localhost:${port}`);
}
bootstrap();
Lifecycle Steps:
  1. The app starts (main.ts is executed)
  2. The root module (AppModule) is loaded
  3. All imported modules are loaded recursively
  4. Controllers and providers are registered
  5. Dependency injection container resolves all dependencies
  6. Global pipes, filters, guards, interceptors are applied
  7. The HTTP server listens for requests
  8. Application is ready to handle requests
Diagram: Application Lifecycle
main.ts

NestFactory.create(AppModule)

AppModule loaded

Imported modules loaded

Providers instantiated

Controllers registered

Routes mapped

HTTP Server listening

Ready to handle requests

1.6 Real-World Example: Building a User API

Let’s build a simple REST API for managing users. This example will show you how the pieces fit together in a real project.

Step 1: Generate Resources

NestJS CLI can generate boilerplate code for you:
# Generate a module
nest generate module users
# or shorthand: nest g mo users

# Generate a controller
nest generate controller users
# or shorthand: nest g co users

# Generate a service
nest generate service users
# or shorthand: nest g s users
CLI Commands: The NestJS CLI has many helpful commands. Use nest --help to see all available commands. The shorthand versions (g for generate, mo for module, co for controller, s for service) save time.

Step 2: Create a DTO (Data Transfer Object)

DTOs define the shape of data for requests and responses. They help with validation and type safety.
// users/dto/create-user.dto.ts
import { IsString, IsEmail, MinLength } from 'class-validator';

export class CreateUserDto {
  @IsString()
  @MinLength(2)
  name: string;

  @IsEmail()
  email: string;
}
Why DTOs?
  • Type safety: TypeScript knows the shape of your data
  • Validation: class-validator decorators validate input
  • Documentation: DTOs document your API contract
  • Transformation: Can transform data automatically

Step 3: Implement the Service

// users/users.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';

@Injectable()
export class UsersService {
  private users = [
    { id: 1, name: 'Alice', email: '[email protected]' },
    { id: 2, name: 'Bob', email: '[email protected]' },
  ];

  findAll() {
    return this.users;
  }

  findOne(id: number) {
    const user = this.users.find(u => u.id === id);
    if (!user) {
      throw new NotFoundException(`User with ID ${id} not found`);
    }
    return user;
  }

  create(createUserDto: CreateUserDto) {
    const id = this.users.length + 1;
    const user = { id, ...createUserDto };
    this.users.push(user);
    return user;
  }

  update(id: number, updateUserDto: Partial<CreateUserDto>) {
    const user = this.findOne(id);  // Throws if not found
    Object.assign(user, updateUserDto);
    return user;
  }

  remove(id: number) {
    const index = this.users.findIndex(u => u.id === id);
    if (index === -1) {
      throw new NotFoundException(`User with ID ${id} not found`);
    }
    this.users.splice(index, 1);
    return { message: `User with ID ${id} deleted` };
  }
}
Key Points:
  • Service handles all business logic
  • Uses NestJS exceptions for error handling
  • Methods are async-ready (can easily add async/await later)
  • Service is stateless (data stored in memory for this example)

Step 4: Implement the Controller

// users/users.controller.ts
import {
  Controller,
  Get,
  Post,
  Put,
  Delete,
  Body,
  Param,
  ParseIntPipe,
} from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';

@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Get()
  findAll() {
    return this.usersService.findAll();
  }

  @Get(':id')
  findOne(@Param('id', ParseIntPipe) id: number) {
    return this.usersService.findOne(id);
  }

  @Post()
  create(@Body() createUserDto: CreateUserDto) {
    return this.usersService.create(createUserDto);
  }

  @Put(':id')
  update(
    @Param('id', ParseIntPipe) id: number,
    @Body() updateUserDto: Partial<CreateUserDto>,
  ) {
    return this.usersService.update(id, updateUserDto);
  }

  @Delete(':id')
  remove(@Param('id', ParseIntPipe) id: number) {
    return this.usersService.remove(id);
  }
}
Key Points:
  • Controller is thin—delegates to service
  • Uses ParseIntPipe to transform and validate route parameters
  • DTOs are used for request body validation
  • HTTP methods map to service methods

Step 5: Wire Up the Module

// users/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 UsersService
})
export class UsersModule {}

Step 6: Import into App Module

// app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UsersModule } from './users/users.module';

@Module({
  imports: [UsersModule],  // Import UsersModule
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Testing the API

Try it out with curl or Postman:
# Get all users
curl http://localhost:3000/users

# Get user by ID
curl http://localhost:3000/users/1

# Create a user
curl -X POST http://localhost:3000/users \
  -H "Content-Type: application/json" \
  -d '{"name": "Charlie", "email": "[email protected]"}'

# Update a user
curl -X PUT http://localhost:3000/users/1 \
  -H "Content-Type: application/json" \
  -d '{"name": "Alice Updated"}'

# Delete a user
curl -X DELETE http://localhost:3000/users/1

1.7 Best Practices & Tips

Following best practices from the start will save you time and prevent issues later.

Code Organization

Feature-based structure:
src/
├── users/
│   ├── dto/
│   │   ├── create-user.dto.ts
│   │   └── update-user.dto.ts
│   ├── users.controller.ts
│   ├── users.service.ts
│   └── users.module.ts
├── auth/
│   └── ...
└── app.module.ts
Benefits:
  • Related code is grouped together
  • Easy to find and maintain
  • Scales well as app grows
  • Clear module boundaries

Use DTOs and Validation

Always validate input using DTOs and class-validator:
import { IsString, IsEmail, MinLength, MaxLength } from 'class-validator';

export class CreateUserDto {
  @IsString()
  @MinLength(2)
  @MaxLength(50)
  name: string;

  @IsEmail()
  email: string;
}
Why?
  • Prevents invalid data from entering your system
  • Clear error messages for API consumers
  • Type safety at runtime
  • Self-documenting API

Keep Controllers Thin

Controllers should only handle HTTP concerns:
// ❌ Bad: Business logic in controller
@Post()
create(@Body() dto: CreateUserDto) {
  if (dto.email.includes('@')) {
    // validation logic
  }
  // business logic
  return { ... };
}

// ✅ Good: Controller delegates to service
@Post()
create(@Body() dto: CreateUserDto) {
  return this.usersService.create(dto);
}

Use Environment Variables

Never hard-code configuration:
// ❌ Bad
const port = 3000;

// ✅ Good
const port = process.env.PORT || 3000;
Use @nestjs/config for better configuration management (covered in later chapters).

Leverage the CLI

The NestJS CLI is your friend:
# Generate resources
nest g resource users

# Generate a guard
nest g guard auth

# Generate an interceptor
nest g interceptor logging

Document Your API

Use Swagger/OpenAPI for API documentation:
npm install @nestjs/swagger
This makes it easy for frontend developers to understand your API.

Handle Errors Gracefully

Use NestJS built-in exceptions:
import { NotFoundException, BadRequestException } from '@nestjs/common';

if (!user) {
  throw new NotFoundException('User not found');
}

Write Tests

Write tests early and often. NestJS makes testing easy (covered in Testing chapter).

1.8 Common Pitfalls and How to Avoid Them

Pitfall 1: Forgetting @Injectable() decorator
// ❌ Bad: Missing @Injectable()
export class UsersService {
  // ...
}

// ✅ Good
@Injectable()
export class UsersService {
  // ...
}
Pitfall 2: Circular dependencies
// ❌ Bad: Module A imports Module B, Module B imports Module A
// Solution: Use forwardRef() or restructure modules
Pitfall 3: Not exporting providers
// ❌ Bad: Service not exported, can't be used in other modules
@Module({
  providers: [UsersService],
})

// ✅ Good: Export what other modules need
@Module({
  providers: [UsersService],
  exports: [UsersService],
})
Pitfall 4: Business logic in controllers Keep controllers thin, move logic to services.

1.9 Summary

You’ve learned the foundational concepts of NestJS:
  • Modules: Organize your code into logical units
  • Controllers: Handle HTTP requests and responses
  • Services: Contain business logic
  • Dependency Injection: Enables loose coupling and testability
  • Application Lifecycle: How NestJS starts and runs
These building blocks form the foundation of every NestJS application. Understanding them deeply will help you build scalable, maintainable applications. Key Takeaways:
  • NestJS brings structure and best practices to Node.js
  • TypeScript-first approach provides type safety
  • Modular architecture makes code maintainable
  • Dependency injection enables testability
  • CLI tools speed up development
Next Steps:
  • Practice building more features using these concepts
  • Learn about Dependency Injection in depth (next chapter)
  • Experiment with the CLI to generate different resources
Next Chapter: Learn how Dependency Injection powers modular, testable code in NestJS. We’ll explore provider scopes, custom providers, dynamic modules, and advanced DI patterns.