Skip to main content

Documentation Index

Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt

Use this file to discover all available pages before exploring further.

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 departments in a company. The HR department (UsersModule) handles everything about employees, the Finance department (PaymentsModule) handles money, and the IT department (SharedModule) provides services everyone needs like email and logging. Each department has its own staff (providers), its own reception desk (controllers), and it decides what services it makes available to other departments (exports). Every app has at least one root module (AppModule), which is like the company’s CEO office — it knows about all the departments and coordinates them. Why Modules Matter:
  • Organization: Group related functionality together
  • Encapsulation: Control what’s exposed to other modules — a module’s providers are private by default, just like a department’s internal processes
  • Reusability: Modules can be imported and reused across different applications
  • Testing: Test modules in isolation, since each module is self-contained
Basic Module Structure:
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';

// @Module() is a class decorator that tells NestJS: "This class defines
// a module." The metadata object describes the module's boundaries --
// what it owns, what it needs, and what it shares.
@Module({
  controllers: [UsersController],  // The "reception desk" -- handles incoming HTTP requests
  providers: [UsersService],       // The "staff" -- services available for injection within this module
  exports: [UsersService],          // The "public API" -- what other modules can use when they import this one
})
export class UsersModule {}
Module Metadata Explained:
  • controllers: Array of controllers that handle HTTP requests for this module. These are automatically registered as route handlers.
  • providers: Services, repositories, factories, or values that can be injected. These are private to the module by default.
  • imports: Other modules whose exported providers you want to use. Think of this as declaring your dependencies — “I need what the AuthModule offers.”
  • exports: Providers from this module that should be available to other modules. If you forget to export, other modules will get a runtime error when they try to inject your provider.
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 the front desk at a hotel. When a guest (HTTP request) arrives, the front desk greets them, figures out what they need (parses the route, query params, body), calls the right department to handle it (delegates to a service), and then relays the answer back to the guest (sends the response). The front desk never cleans the room itself — that is the service’s job. 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') sets the base route prefix. Every method decorator
// below builds on this, so @Get() becomes GET /users, @Get(':id') becomes
// GET /users/:id, and so on.
@Controller('users')
export class UsersController {
  // NestJS sees UsersService in the constructor and automatically injects
  // the singleton instance. You never call "new UsersService()" yourself.
  // The 'private readonly' shorthand both declares and assigns the property.
  constructor(private readonly usersService: UsersService) {}

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

  @Get(':id')  // GET /users/:id -- the colon means this segment is dynamic
  findOne(@Param('id') id: string) {
    // @Param('id') extracts the :id segment from the URL.
    // Note: it arrives as a string. Use ParseIntPipe for automatic conversion.
    return this.usersService.findOne(id);
  }

  @Post()  // POST /users -- creates a new user from the request body
  create(@Body() createUserDto: CreateUserDto) {
    // @Body() extracts and deserializes the JSON request body into
    // a CreateUserDto instance. With ValidationPipe enabled globally,
    // this also validates the data before your code ever runs.
    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. Think of the NestJS DI container as a service desk at a large office: you submit a request (“I need a UsersService”), and the service desk either hands you the existing one (singleton) or creates a fresh one (transient/request-scoped). You never walk into the back room and build it yourself. Services are the most common type of provider. They contain the business logic that controllers delegate to. But providers can also be repositories, helpers, configuration objects, or anything else your code needs. What Makes a Provider:
  • Any class decorated with @Injectable() — this decorator tells NestJS “you can manage my lifecycle and inject me”
  • Can be injected into controllers or other providers via constructor parameters
  • Managed by NestJS dependency injection container — you never call new yourself
  • Can have different scopes (singleton, request-scoped, transient) depending on your needs
Service Example:
import { Injectable } from '@nestjs/common';

// @Injectable() registers this class with the NestJS DI container.
// Without it, NestJS cannot resolve or inject this class -- you will
// get a cryptic "Nest can't resolve dependencies" error at startup.
@Injectable()
export class UsersService {
  // In-memory data for demonstration. In production, this would be
  // replaced by a repository that talks to a real database.
  private users = [
    { id: 1, name: 'Alice', email: 'alice@example.com' },
    { id: 2, name: 'Bob', email: 'bob@example.com' },
  ];

  findAll() {
    return this.users;
  }

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

  create(userData: { name: string; email: string }) {
    // WARNING: Using array length + 1 for IDs is fine for demos but
    // will produce duplicates after deletions. In production, use
    // auto-incrementing database IDs or UUIDs.
    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).

NestJS Building Blocks at a Glance

Before diving deeper, here is a quick reference for how the core building blocks relate:
Building BlockResponsibilityDecoratorRegistered InInjected Via
ModuleOrganizes related code, defines boundaries@Module()imports array of parent moduleN/A (imported, not injected)
ControllerHandles HTTP requests, defines routes@Controller()controllers array of its moduleConstructor (services)
ServiceBusiness logic, orchestration@Injectable()providers array of its moduleConstructor (other providers)
RepositoryData access abstraction@Injectable()providers array of its moduleConstructor (ORM repositories)
GuardAuthentication / authorization gate@Injectable()Applied via @UseGuards()Constructor (services)
PipeValidation / transformation of input@Injectable()Applied via @UsePipes() or per-paramConstructor (services)
InterceptorPre/post-processing, response wrapping@Injectable()Applied via @UseInterceptors()Constructor (services)
FilterException handling, error formatting@Catch()Applied via @UseFilters()Constructor (services)
MiddlewareRaw request/response processingClass or functionconfigure() method in moduleConstructor (services, class-based only)
Decision Framework — Where Does My Code Go? If it is HTTP-specific (routing, status codes), put it in a controller. If it is a business rule, put it in a service. If it validates or transforms input, make it a pipe. If it gates access, make it a guard. If it wraps or transforms the response, make it an interceptor. If it catches errors, make it a filter. If it needs raw req/res and runs before everything else, make it middleware.

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: 'alice@example.com' },
    { id: 2, name: 'Bob', email: 'bob@example.com' },
  ];

  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": "charlie@example.com"}'

# 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 Edge Cases New Projects Hit Early

These are situations that tutorials skip but real projects encounter within the first week. Edge Case 1: Multiple controllers with overlapping routes If you have @Controller('users') with @Get(':id') and also @Get('search'), NestJS evaluates routes top-to-bottom. The :id wildcard will match “search” before the literal “search” route gets a chance. Always place specific routes above parameterized routes in the controller.
@Controller('users')
export class UsersController {
  @Get('search')        // Must come BEFORE :id, or 'search' is treated as an id
  search(@Query('q') q: string) { ... }

  @Get(':id')           // Catches everything not matched above
  findOne(@Param('id', ParseIntPipe) id: number) { ... }
}
Edge Case 2: Async module initialization order If Module A depends on Module B’s providers, and Module B uses forRootAsync() with a factory that fetches config from a remote source, Module A may try to inject before Module B is ready. NestJS handles this via the module dependency graph — if you list Module B in Module A’s imports, it will be initialized first. But if you forget the import and rely on @Global(), initialization order is not guaranteed for async providers. Edge Case 3: Hot module reload loses singleton state When using start:dev with hot module replacement, singleton services are re-instantiated on every code change. If you are storing state in a singleton (like an in-memory cache during development), it resets on every save. This is not a bug — it is HMR working as designed. Use an external store (Redis, database) even in development if state persistence matters. Edge Case 4: Fastify vs Express differences NestJS supports both Express and Fastify as underlying HTTP platforms. Most tutorials assume Express, but if you switch to Fastify for performance, be aware: @Res() gives you a Fastify Reply object, not an Express Response. Middleware that uses Express-specific APIs (res.send, res.json overrides) will break. Check your middleware compatibility before switching.

1.8 Common Pitfalls and How to Avoid Them

Pitfall 1: Forgetting @Injectable() decorator This is the single most common NestJS beginner error. The error message “Nest can’t resolve dependencies of UsersController (?)” usually means one of the constructor parameters is missing @Injectable().
// ❌ Bad: Missing @Injectable() -- NestJS cannot manage this class
export class UsersService {
  // ...
}

// ✅ Good: Decorator tells NestJS to track this class in the DI container
@Injectable()
export class UsersService {
  // ...
}
Pitfall 2: Circular dependencies Two modules importing each other is a design smell. If AuthModule needs UsersModule and UsersModule needs AuthModule, consider extracting the shared logic into a third module (e.g., SharedUserAuthModule) that both can import.
// ❌ Bad: Module A imports Module B, Module B imports Module A
// This causes a runtime error unless you use forwardRef()

// ✅ Better: Use forwardRef() as a last resort
@Module({
  imports: [forwardRef(() => AuthModule)],
})
export class UsersModule {}

// ✅ Best: Restructure to eliminate the cycle entirely
Pitfall 3: Not exporting providers If another module imports yours but cannot inject your service, you almost certainly forgot to add it to exports. NestJS providers are module-private by default — this is intentional encapsulation, not a bug.
// ❌ Bad: Service not exported, other modules get injection errors
@Module({
  providers: [UsersService],
})

// ✅ Good: Export what other modules need
@Module({
  providers: [UsersService],
  exports: [UsersService],
})
Pitfall 4: Business logic in controllers If your controller method is longer than 5-10 lines, it is probably doing too much. Controllers should parse the request, call a service, and return the result. Validation, data transformation, and business rules belong in services or pipes. Pitfall 5: Not using ParseIntPipe for route parameters Route parameters arrive as strings. Forgetting to convert them leads to subtle bugs where findOne("1") does not match user.id === 1.
// ❌ Bad: id is a string, comparison with number fails silently
@Get(':id')
findOne(@Param('id') id: string) {
  return this.usersService.findOne(id);  // comparing string to number
}

// ✅ Good: ParseIntPipe converts and validates in one step
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
  return this.usersService.findOne(id);  // id is guaranteed to be a number
}

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.

Interview Deep-Dive

Strong Answer:
  • NestFactory.create(AppModule) triggers a multi-step initialization. First, NestJS instantiates the IoC (Inversion of Control) container. Then it reads the @Module() decorator metadata from AppModule using Reflect.getMetadata, which returns the imports, controllers, providers, and exports arrays.
  • It recursively loads all imported modules in a depth-first manner. For each module, it registers the providers in the DI container. The key detail: providers are not instantiated yet at this point — they are just registered. NestJS uses lazy instantiation for most providers, creating them only when they are first injected.
  • After all modules are loaded, the container resolves the dependency graph. It reads design:paramtypes metadata from each provider’s constructor (set by TypeScript’s emitDecoratorMetadata compiler option) to determine what each class needs. If a dependency is missing or circular without forwardRef, this is where the “Nest can’t resolve dependencies” error fires.
  • Controllers are then registered, and their route decorators (@Get, @Post, etc.) are read to build the internal route map. Middleware, global guards, interceptors, and pipes are attached to the appropriate routes.
  • Finally, app.listen(3000) starts the underlying HTTP server (Express or Fastify). At this point, the application is ready to handle requests.
  • The critical production detail: if any async provider (a factory provider with useFactory: async () => ...) fails during initialization, the entire bootstrap fails. This is actually desirable — you want the app to crash at startup if the database connection fails, not silently start serving 500 errors.
Follow-up: What happens if you have a module that takes 30 seconds to initialize (say, connecting to a slow external service)? How does that affect the bootstrap?The entire bootstrap blocks until all async providers resolve. NestFactory.create() returns a Promise that does not resolve until every module is initialized. In Kubernetes, this means your readiness probe will fail during that 30 seconds, which is correct behavior — the pod should not receive traffic until it is fully initialized. The fix is to set initialDelaySeconds on your readiness probe high enough to accommodate slow providers. If the 30-second initialization is unacceptable, consider making the connection lazy — initialize the provider as a singleton but defer the actual connection until the first request, using an onModuleInit hook that runs in the background.
Strong Answer:
  • The immediate problem is testability. To test that controller method, you now need a real database connection or a complex mock of the database client. If the logic were in a service, you could mock the service with one line: { provide: UsersService, useValue: { findAll: jest.fn() } }.
  • The second problem is reusability. If another controller or a scheduled job needs the same query, you duplicate code. With a service, you call usersService.findAll() from anywhere.
  • The third problem is separation of concerns. The controller is now responsible for HTTP concerns (routing, status codes, response format) AND business logic (query construction, data filtering). When the query needs to change, you are editing a file that is also responsible for routing, which increases the blast radius of the change and makes code review harder.
  • The fourth problem, which most people miss, is that it breaks NestJS’s DI-based architecture. Interceptors can transform the response from a controller method, but if the controller is directly calling the database, the interceptor cannot easily cache or transform the database query — it only sees the final result. With a service layer, you can add caching at the service level independently.
  • Refactoring: extract the query into a service method, inject the service into the controller, and have the controller delegate to the service. If the query is complex, add a repository layer between the service and the database. The controller method should be 1-3 lines: validate input (via pipes), call service, return result.
Follow-up: Is there ever a case where putting logic in the controller is acceptable?Yes, but only for HTTP-specific logic that does not belong in the service. For example, setting a custom response header, redirecting to a different URL, or streaming a file download. These are HTTP concerns that the service should not know about. The service returns data, and the controller decides how to deliver it over HTTP. But even in these cases, the business logic (determining which file to serve, or which URL to redirect to) should be in the service.
Strong Answer:
  • TypeScript decorators are functions that run at class definition time (not at runtime when you create instances). When you write @Injectable(), the decorator function is called with the class as an argument. NestJS’s @Injectable() does not do much itself — it mainly exists so that TypeScript emits metadata for the class.
  • The real magic is emitDecoratorMetadata: true in tsconfig.json. When this is enabled, TypeScript automatically calls Reflect.defineMetadata('design:paramtypes', [...], TargetClass) for every class that has at least one decorator. This metadata records the types of all constructor parameters.
  • So when NestJS sees constructor(private usersService: UsersService), it calls Reflect.getMetadata('design:paramtypes', ControllerClass) and gets back [UsersService]. It then looks up UsersService in the DI container and injects the instance.
  • If you set emitDecoratorMetadata: false, NestJS cannot determine constructor parameter types. Every injection would fail with “Nest can’t resolve dependencies” because the framework has no way to know what to inject. You would have to use explicit @Inject() decorators on every constructor parameter, which defeats the purpose of automatic DI.
  • There is a subtle edge case: if a constructor parameter’s type is an interface (not a class), design:paramtypes records it as Object because interfaces are erased at runtime in TypeScript. This is why you need @Inject('TOKEN') for interface-based injection — the metadata cannot distinguish between different interfaces.
Follow-up: The NestJS team has discussed moving away from reflect-metadata toward TypeScript 5’s native decorator support. What would that change?TypeScript 5 introduced TC39 Stage 3 decorators, which are a different specification than the experimental decorators NestJS currently uses. The key difference is that TC39 decorators do not support emitDecoratorMetadata — there is no automatic parameter type emission. This means NestJS would need to move toward explicit injection tokens or a different metadata strategy (like Prisma’s approach of code generation, or Angular’s approach of using a compiler plugin). The NestJS team has been cautious about this migration because it would be a breaking change for every existing NestJS application. For now, NestJS still requires experimentalDecorators: true in tsconfig.json, and the TC39 decorator migration is a future concern.
Strong Answer:
  • This is almost always a build or path resolution issue. The first thing I check is whether npm run build completes without errors locally. NestJS compiles TypeScript to JavaScript in the dist/ folder, and the production server runs node dist/main.js — it never sees .ts files.
  • The most common cause is a missing import that TypeScript resolves at compile time but fails at runtime. For example, if you use a path alias like @app/users in tsconfig.json but do not configure the same alias in the build output, the compiled JavaScript will still have require('@app/users'), which Node.js cannot resolve. The fix is to use tsconfig-paths in production or switch to relative imports.
  • The second common cause is a dynamic import or a file path reference (like entities: [__dirname + '/**/*.entity{.ts,.js}'] in TypeORM config). In development, __dirname points to the src/ directory. In production, it points to dist/. If the glob pattern includes .ts but the dist/ folder only has .js files, the entity is never loaded.
  • The third cause is missing dependencies. npm ci --only=production skips devDependencies. If your code accidentally imports a dev-only package (like @nestjs/testing in a production file), it will crash. Check your package.json to ensure all runtime dependencies are in dependencies, not devDependencies.
  • My debugging checklist: (1) Build locally, copy dist/ and node_modules to a clean directory, run node dist/main.js. (2) Check for path alias issues in compiled .js files. (3) Verify entity/migration glob patterns work with the dist/ directory structure. (4) Check that the Docker COPY stage includes all necessary files.
Follow-up: How do you prevent this class of issue from recurring in CI?Add a CI step that builds the production image and runs a smoke test. After docker build, execute docker run --rm myapp node dist/main.js --dry-run (or a health check curl). If the app crashes at startup, CI fails before the deployment step. I also add a pre-commit check that runs npm run build to catch TypeScript compilation errors early. For path aliases, I use tsc-alias as a post-build step to rewrite aliases to relative paths in the compiled output, eliminating the entire class of path resolution bugs.