06.02.2026 β€’ 13 min read

NestJS Masterclass | From Zero to Production with the Right Architecture

Cover Image

Introduction 🎯

NestJS is a progressive Node.js framework for building efficient, scalable server-side applications. It uses TypeScript by default, provides built-in dependency injection, and enforces architectural patterns that scale from tiny APIs to enterprise systems.

This guide follows NestJS’s official documentation structureβ€”OVERVIEW through FUNDAMENTALSβ€”but with practical, hands-on examples. We’ll build real things, understand why each decision matters, and equip you to architect production applications.

By the end, you’ll understand how NestJS enforces good architecture and prevents common backend mistakes. Let’s build. πŸš€


Part 1: OVERVIEWβ€”Why NestJS Matters

1.1 What Makes NestJS Different?

Express vs Fastify vs NestJS:

(#)            Express       Fastify       NestJS
Architecture    πŸ“¦ Minimal    πŸ“¦ Minimal    πŸ—οΈ  Opinionated
Patterns        No pattern    No pattern    βœ… Built-in
DI Container    ❌ Manual     ❌ Manual     βœ… Automatic
Testing         😞 Hard       😞 Hard       βœ… Easy
Scale           πŸ“ˆ With work  πŸ“ˆ With work  βœ… Automatic

NestJS gives you:

  1. Architecture out of the box: Modules, Controllers, Services, Providersβ€”no decisions needed
  2. Dependency Injection: Never pass dependencies manually; the framework handles it
  3. Testability: Mock providers easily; tests are simple
  4. Type Safety: Full TypeScript support (not optional)
  5. Scalability: Works the same way from 100 requests/sec to 100K

1.2 When to Use NestJS

βœ… Use NestJS when:

  • Building production APIs that will grow
  • Team size > 1 person (consistency matters)
  • You want TypeScript without fighting the framework
  • You need dependency injection
  • Building microservices

❌ Skip NestJS if:

  • Building a simple CRUD API in 30 minutes (use Express)
  • You hate boilerplate (there’s a little)
  • You’re learning Node.js basics (start with Express first)
  • Building real-time with WebSockets only (possible but extra work)

1.3 NestJS Architecture Overview

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚           HTTP Request                              β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                       ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Middleware (cors, logging, etc)                    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                       ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Guards (authentication, authorization)             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                       ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Interceptors (transform, cache, timing)            β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                       ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Pipes (validation, transformation)                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                       ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Controller (route to service)                      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                       ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Service (business logic)                           β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                       ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Response                                           β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Part 2: FUNDAMENTALSβ€”Building Your First NestJS App

2.1 Installation and Project Setup

# Install NestJS CLI (one-time)
npm i -g @nestjs/cli

# Create new project
nest new my-app
cd my-app

# Project structure
my-app/
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ app.controller.ts       # Main controller
β”‚   β”œβ”€β”€ app.service.ts          # Main service
β”‚   β”œβ”€β”€ app.module.ts           # Root module
β”‚   └── main.ts                 # Entry point
β”œβ”€β”€ test/                       # End-to-end tests
β”œβ”€β”€ package.json
└── tsconfig.json
# Run development server
npm run start:dev

# Server runs on http://localhost:3000

2.2 Modulesβ€”Organizing Your Code

A module bundles related controllers, services, and other providers:

// 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],  // Available to other modules
})
export class UsersModule {}
// app.module.ts (root module)
import { Module } from "@nestjs/common";
import { UsersModule } from "./users/users.module";
import { PostsModule } from "./posts/posts.module";

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

2.3 Controllersβ€”Handle Requests

Controllers receive requests and delegate to services:

// users/users.controller.ts
import { Controller, Get, Post, Body, Param } from "@nestjs/common";
import { UsersService } from "./users.service";

interface CreateUserDTO {
  email: string;
  name: string;
  password: string;
}

interface UserResponse {
  id: number;
  email: string;
  name: string;
}

@Controller("users")  // Route prefix: /users
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  // πŸ“– GET /users
  @Get()
  async getAllUsers(): Promise<UserResponse[]> {
    return this.usersService.findAll();
  }

  // πŸ“– GET /users/:id
  @Get(":id")
  async getUserById(@Param("id") id: string): Promise<UserResponse> {
    return this.usersService.findById(parseInt(id));
  }

  // πŸ“ POST /users
  @Post()
  async createUser(@Body() createUserDTO: CreateUserDTO): Promise<UserResponse> {
    return this.usersService.create(createUserDTO);
  }
}

Key decorators:

  • @Get(), @Post(), @Patch(), @Delete() β€” HTTP methods
  • @Param() β€” Route parameters (:id)
  • @Query() β€” Query string (?page=1)
  • @Body() β€” Request body
  • @Headers() β€” HTTP headers

2.4 Servicesβ€”Business Logic

Services contain reusable logic. Controllers call services:

// users/users.service.ts
import { Injectable } from "@nestjs/common";

interface User {
  id: number;
  email: string;
  name: string;
  passwordHash: string;
}

@Injectable()  // Can be injected into other classes
export class UsersService {
  private users: User[] = [];  // Mock database
  private idCounter = 1;

  async findAll(): Promise<Omit<User, "passwordHash">[]> {
    return this.users.map(({ passwordHash, ...user }) => user);
  }

  async findById(id: number): Promise<Omit<User, "passwordHash"> | null> {
    const user = this.users.find((u) => u.id === id);
    if (!user) return null;

    const { passwordHash, ...safe } = user;
    return safe;
  }

  async create(data: {
    email: string;
    name: string;
    password: string;
  }): Promise<Omit<User, "passwordHash">> {
    const user: User = {
      id: this.idCounter++,
      email: data.email,
      name: data.name,
      passwordHash: hashPassword(data.password),  // Never store plain text!
    };

    this.users.push(user);

    const { passwordHash, ...safe } = user;
    return safe;
  }
}

function hashPassword(password: string): string {
  // Use bcrypt in real app
  return Buffer.from(password).toString("base64");
}

2.5 Dependency Injectionβ€”The Heart of NestJS

NestJS automatically provides dependencies:

// posts/posts.service.ts
import { Injectable } from "@nestjs/common";
import { UsersService } from "../users/users.service";

@Injectable()
export class PostsService {
  constructor(private readonly usersService: UsersService) {}
  // UsersService is automatically injected!

  async createPost(userId: number, title: string, content: string) {
    const user = await this.usersService.findById(userId);
    if (!user) throw new Error("User not found");

    return {
      id: 1,
      userId,
      title,
      content,
      createdAt: new Date(),
    };
  }
}

Why DI matters:

  • βœ… Tests: Easy to mock dependencies
  • βœ… Loose coupling: Services don’t create each other
  • βœ… Configuration: Swap implementations without code changes

Part 3: Advanced Fundamentals

3.1 Providersβ€”Beyond Services

A provider is anything NestJS can inject:

// database.provider.ts
import { Provider } from "@nestjs/common";

export const databaseProvider: Provider = {
  provide: "DATABASE_CONNECTION",  // Unique token
  useValue: createDatabaseConnection(),  // Provide the instance
};

function createDatabaseConnection() {
  return {
    query: async (sql: string) => {
      /* ... */
    },
  };
}

// database.module.ts
import { Module } from "@nestjs/common";

@Module({
  providers: [databaseProvider],
  exports: [databaseProvider],
})
export class DatabaseModule {}
// users/users.service.ts
import { Inject, Injectable } from "@nestjs/common";

@Injectable()
export class UsersService {
  constructor(@Inject("DATABASE_CONNECTION") private db: any) {}

  async findAll() {
    return this.db.query("SELECT * FROM users");
  }
}

3.2 Pipesβ€”Validation and Transformation

Pipes process request data before it reaches controllers:

// common/pipes/validation.pipe.ts
import { ArgumentMetadata, BadRequestException, PipeTransform } from "@nestjs/common";
import { plainToInstance } from "class-transformer";
import { validate } from "class-validator";

export class ValidationPipe implements PipeTransform {
  async transform(value: any, metadata: ArgumentMetadata) {
    const targetType = metadata.type;
    const dto = plainToInstance(targetType, value);
    const errors = await validate(dto);

    if (errors.length > 0) {
      throw new BadRequestException({
        message: "Validation failed",
        errors: errors.map((err) => ({
          field: err.property,
          messages: Object.values(err.constraints || {}),
        })),
      });
    }

    return value;
  }
}
// users/dto/create-user.dto.ts
import { IsEmail, IsString, MinLength } from "class-validator";

export class CreateUserDTO {
  @IsEmail()
  email!: string;

  @IsString()
  @MinLength(2)
  name!: string;

  @IsString()
  @MinLength(8)
  password!: string;
}
// users/users.controller.ts
import { UsePipes } from "@nestjs/common";
import { ValidationPipe } from "../common/pipes/validation.pipe";
import { CreateUserDTO } from "./dto/create-user.dto";

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

  @Post()
  @UsePipes(new ValidationPipe())
  async createUser(@Body() createUserDTO: CreateUserDTO) {
    return this.usersService.create(createUserDTO);
  }
}

3.3 Guardsβ€”Authentication and Authorization

Guards determine if a request should proceed:

// common/guards/auth.guard.ts
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private readonly jwtService: JwtService) {}

  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    const token = request.headers.authorization?.split(" ")[1];

    if (!token) {
      throw new UnauthorizedException("Missing token");
    }

    try {
      const decoded = this.jwtService.verify(token);
      request.user = decoded;
      return true;
    } catch (err) {
      throw new UnauthorizedException("Invalid token");
    }
  }
}

// common/guards/roles.guard.ts
import { CanActivate, ExecutionContext, ForbiddenException, Injectable } from "@nestjs/common";
import { Reflector } from "@nestjs/core";

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.get<string[]>("roles", context.getHandler());

    if (!requiredRoles) return true;  // No roles required

    const request = context.switchToHttp().getRequest();
    const userRole = request.user?.role;

    if (!requiredRoles.includes(userRole)) {
      throw new ForbiddenException("Insufficient permissions");
    }

    return true;
  }
}
// common/decorators/roles.decorator.ts
import { SetMetadata } from "@nestjs/common";

export const Roles = (...roles: string[]) => SetMetadata("roles", roles);
// users/users.controller.ts
import { UseGuards } from "@nestjs/common";
import { Roles } from "../common/decorators/roles.decorator";
import { AuthGuard } from "../common/guards/auth.guard";
import { RolesGuard } from "../common/guards/roles.guard";

@Controller("users")
export class UsersController {
  // ...

  @Delete(":id")
  @UseGuards(AuthGuard, RolesGuard)
  @Roles("admin")
  async deleteUser(@Param("id") id: string) {
    return this.usersService.delete(parseInt(id));
  }
}

3.4 Interceptorsβ€”Transform Responses

Interceptors modify responses before sending them:

// common/interceptors/response.interceptor.ts
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from "@nestjs/common";
import { map } from "rxjs/operators";

interface ApiResponse<T> {
  success: boolean;
  data: T;
  timestamp: number;
}

@Injectable()
export class ResponseInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler) {
    return next.handle().pipe(
      map((data) => ({
        success: true,
        data,
        timestamp: Date.now(),
      }))
    );
  }
}

// common/interceptors/logging.interceptor.ts
import { CallHandler, ExecutionContext, Injectable, Logger, NestInterceptor } from "@nestjs/common";
import { tap } from "rxjs/operators";

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  private logger = new Logger("HTTP");

  intercept(context: ExecutionContext, next: CallHandler) {
    const request = context.switchToHttp().getRequest();
    const { method, url } = request;
    const start = Date.now();

    return next.handle().pipe(
      tap(() => {
        const duration = Date.now() - start;
        this.logger.log(`${method} ${url} - ${duration}ms`);
      })
    );
  }
}
// app.module.ts
import { Module } from "@nestjs/common";
import { APP_INTERCEPTOR } from "@nestjs/core";
import { ResponseInterceptor } from "./common/interceptors/response.interceptor";
import { LoggingInterceptor } from "./common/interceptors/logging.interceptor";

@Module({
  providers: [
    { provide: APP_INTERCEPTOR, useClass: ResponseInterceptor },
    { provide: APP_INTERCEPTOR, useClass: LoggingInterceptor },
  ],
})
export class AppModule {}

3.5 Middlewareβ€”Request Processing

Middleware runs before guards/pipes:

// common/middleware/correlation-id.middleware.ts
import { Injectable, NestMiddleware } from "@nestjs/common";
import { Request, Response, NextFunction } from "express";

@Injectable()
export class CorrelationIdMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    const correlationId = req.headers["x-correlation-id"] || `${Date.now()}:${Math.random()}`;
    req.id = correlationId;

    res.setHeader("x-correlation-id", correlationId);
    next();
  }
}

// app.module.ts
import { Module, NestModule, MiddlewareConsumer } from "@nestjs/common";
import { CorrelationIdMiddleware } from "./common/middleware/correlation-id.middleware";

@Module({})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(CorrelationIdMiddleware).forRoutes("*");
  }
}

Part 4: Building a Complete Feature

4.1 Blog Module Example

# Generate with CLI
nest generate module blog
nest generate controller blog
nest generate service blog

# Creates structure:
src/blog/
β”œβ”€β”€ blog.module.ts
β”œβ”€β”€ blog.controller.ts
β”œβ”€β”€ blog.service.ts
└── dto/
    β”œβ”€β”€ create-post.dto.ts
    └── update-post.dto.ts
// blog/dto/create-post.dto.ts
import { IsString, MinLength } from "class-validator";

export class CreatePostDTO {
  @IsString()
  @MinLength(5)
  title!: string;

  @IsString()
  @MinLength(20)
  content!: string;
}

// blog/blog.service.ts
import { Injectable } from "@nestjs/common";
import { CreatePostDTO } from "./dto/create-post.dto";

interface Post {
  id: number;
  title: string;
  content: string;
  createdAt: Date;
  updatedAt: Date;
}

@Injectable()
export class BlogService {
  private posts: Post[] = [];
  private idCounter = 1;

  async getAllPosts(): Promise<Post[]> {
    return this.posts;
  }

  async getPostById(id: number): Promise<Post | null> {
    return this.posts.find((p) => p.id === id) || null;
  }

  async createPost(data: CreatePostDTO): Promise<Post> {
    const post: Post = {
      id: this.idCounter++,
      title: data.title,
      content: data.content,
      createdAt: new Date(),
      updatedAt: new Date(),
    };

    this.posts.push(post);
    return post;
  }

  async updatePost(id: number, data: Partial<CreatePostDTO>): Promise<Post | null> {
    const post = this.posts.find((p) => p.id === id);
    if (!post) return null;

    Object.assign(post, data, { updatedAt: new Date() });
    return post;
  }

  async deletePost(id: number): Promise<boolean> {
    const index = this.posts.findIndex((p) => p.id === id);
    if (index === -1) return false;

    this.posts.splice(index, 1);
    return true;
  }
}

// blog/blog.controller.ts
import { Controller, Get, Post, Body, Param, Patch, Delete, UsePipes } from "@nestjs/common";
import { BlogService } from "./blog.service";
import { CreatePostDTO } from "./dto/create-post.dto";
import { ValidationPipe } from "../common/pipes/validation.pipe";

@Controller("blog/posts")
export class BlogController {
  constructor(private readonly blogService: BlogService) {}

  @Get()
  async getAllPosts() {
    return this.blogService.getAllPosts();
  }

  @Get(":id")
  async getPostById(@Param("id") id: string) {
    const post = await this.blogService.getPostById(parseInt(id));
    if (!post) throw new Error("Post not found");
    return post;
  }

  @Post()
  @UsePipes(new ValidationPipe())
  async createPost(@Body() createPostDTO: CreatePostDTO) {
    return this.blogService.createPost(createPostDTO);
  }

  @Patch(":id")
  @UsePipes(new ValidationPipe())
  async updatePost(@Param("id") id: string, @Body() data: Partial<CreatePostDTO>) {
    const post = await this.blogService.updatePost(parseInt(id), data);
    if (!post) throw new Error("Post not found");
    return post;
  }

  @Delete(":id")
  async deletePost(@Param("id") id: string) {
    const deleted = await this.blogService.deletePost(parseInt(id));
    if (!deleted) throw new Error("Post not found");
    return { message: "Post deleted" };
  }
}

// blog/blog.module.ts
import { Module } from "@nestjs/common";
import { BlogController } from "./blog.controller";
import { BlogService } from "./blog.service";

@Module({
  controllers: [BlogController],
  providers: [BlogService],
  exports: [BlogService],
})
export class BlogModule {}

// app.module.ts
import { Module } from "@nestjs/common";
import { BlogModule } from "./blog/blog.module";

@Module({
  imports: [BlogModule],
})
export class AppModule {}

4.2 Database Integration

// database/database.service.ts
import { Injectable } from "@nestjs/common";
import { Pool } from "pg";

@Injectable()
export class DatabaseService {
  private pool = new Pool({
    host: process.env.DB_HOST,
    port: parseInt(process.env.DB_PORT || "5432"),
    database: process.env.DB_NAME,
    user: process.env.DB_USER,
    password: process.env.DB_PASSWORD,
  });

  async query(sql: string, params: any[] = []) {
    const result = await this.pool.query(sql, params);
    return result.rows;
  }

  async queryOne(sql: string, params: any[] = []) {
    const result = await this.pool.query(sql, params);
    return result.rows[0] || null;
  }

  async close() {
    await this.pool.end();
  }
}

// database/database.module.ts
import { Module } from "@nestjs/common";
import { DatabaseService } from "./database.service";

@Module({
  providers: [DatabaseService],
  exports: [DatabaseService],
})
export class DatabaseModule {}

// blog/blog.service.ts (with database)
import { Injectable } from "@nestjs/common";
import { DatabaseService } from "../database/database.service";
import { CreatePostDTO } from "./dto/create-post.dto";

@Injectable()
export class BlogService {
  constructor(private readonly db: DatabaseService) {}

  async getAllPosts() {
    return this.db.query("SELECT * FROM posts ORDER BY created_at DESC");
  }

  async getPostById(id: number) {
    return this.db.queryOne("SELECT * FROM posts WHERE id = $1", [id]);
  }

  async createPost(data: CreatePostDTO) {
    return this.db.queryOne(
      "INSERT INTO posts (title, content, created_at, updated_at) VALUES ($1, $2, NOW(), NOW()) RETURNING *",
      [data.title, data.content]
    );
  }

  async updatePost(id: number, data: Partial<CreatePostDTO>) {
    const updates = Object.keys(data)
      .map((key, i) => `${key} = $${i + 2}`)
      .join(",");

    return this.db.queryOne(
      `UPDATE posts SET ${updates}, updated_at = NOW() WHERE id = $1 RETURNING *`,
      [id, ...Object.values(data)]
    );
  }

  async deletePost(id: number) {
    const result = await this.db.query("DELETE FROM posts WHERE id = $1 RETURNING id", [id]);
    return result.length > 0;
  }
}

Part 5: Testing

5.1 Unit Testing Services

// blog/blog.service.spec.ts
import { Test, TestingModule } from "@nestjs/testing";
import { BlogService } from "./blog.service";
import { DatabaseService } from "../database/database.service";

describe("BlogService", () => {
  let service: BlogService;
  let mockDatabase: any;

  beforeEach(async () => {
    // Mock the database
    mockDatabase = {
      query: jest.fn(),
      queryOne: jest.fn(),
    };

    const module: TestingModule = await Test.createTestingModule({
      providers: [
        BlogService,
        {
          provide: DatabaseService,
          useValue: mockDatabase,
        },
      ],
    }).compile();

    service = module.get<BlogService>(BlogService);
  });

  it("should create a post", async () => {
    const post = {
      id: 1,
      title: "Test",
      content: "Test content",
      created_at: new Date(),
    };

    mockDatabase.queryOne.mockResolvedValue(post);

    const result = await service.createPost({ title: "Test", content: "Test content" });

    expect(result).toEqual(post);
    expect(mockDatabase.queryOne).toHaveBeenCalled();
  });
});

5.2 E2E Testing

// test/blog.e2e-spec.ts
import { Test, TestingModule } from "@nestjs/testing";
import { INestApplication } from "@nestjs/common";
import * as request from "supertest";
import { AppModule } from "../src/app.module";

describe("Blog (e2e)", () => {
  let app: INestApplication;

  beforeAll(async () => {
    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();

    app = moduleFixture.createNestApplication();
    await app.init();
  });

  afterAll(async () => {
    await app.close();
  });

  it("should create and retrieve a post", async () => {
    const createResponse = await request(app.getHttpServer())
      .post("/blog/posts")
      .send({ title: "Test Post", content: "This is a test post content" })
      .expect(201);

    const postId = createResponse.body.id;

    const getResponse = await request(app.getHttpServer())
      .get(`/blog/posts/${postId}`)
      .expect(200);

    expect(getResponse.body.title).toBe("Test Post");
  });
});

🎯 NestJS Best Practices Checklist

βœ… Use modules to organize features (one feature = one module)
βœ… Keep business logic in services, not controllers
βœ… Use DTOs for input validation
βœ… Use pipes for validation (@UsePipes)
βœ… Use guards for authentication/authorization
βœ… Use interceptors for response transformation
βœ… Implement error handling globally (exception filters)
βœ… Inject dependencies, don't create them manually
βœ… Write unit tests for services
βœ… Write e2e tests for critical flows
βœ… Use environment variables for config
βœ… Never commit secrets

❌ Don't put logic in controllers
❌ Don't mix concerns (service should not know HTTP)
❌ Don't manually instantiate providers
❌ Don't skip validation
❌ Don't leave error handling to Express
❌ Don't test implementation details

Conclusion πŸš€

NestJS isn’t magicβ€”it’s well-organized conventions enforced by the framework. Modules keep code organized. Services contain logic. Controllers handle HTTP. Guards protect routes. Pipes validate input.

Start small with a single module. Build a feature completely. Then duplicate the pattern for the next feature. This is how NestJS scales from solo projects to enterprise applications.

The structure you write today is the structure that scales effortlessly. That’s why NestJS matters. πŸ’ͺ