DEV Community

Cover image for 🚀 Mastering NestJS Concepts: Decorators, Dependency Injection, DTOs & More
Fazal Mansuri
Fazal Mansuri

Posted on

🚀 Mastering NestJS Concepts: Decorators, Dependency Injection, DTOs & More

NestJS is one of the most popular Node.js frameworks, combining TypeScript, OOP principles and modular design. It provides powerful tools like decorators, dependency injection and interceptors, which simplify building scalable applications.

In this blog, we’ll explore some core concepts of NestJS and tackle a real-world issue developers often face with DTOs and getters.


1️⃣ What Are Decorators in NestJS?

Decorators are metadata annotations that NestJS uses to add behavior to classes, methods and properties.

Examples:

  • @Controller() → Marks a class as a controller.
  • @Get(), @Post() → Define route handlers.
  • @Injectable() → Marks a class as available for dependency injection.

➡️ Best practice: Always use NestJS decorators instead of manually wiring routes or dependencies - it keeps code clean and testable.


2️⃣ Dependency Injection (DI) in NestJS

Dependency Injection is a design pattern where objects (services) are injected into a class instead of being created manually.

Example:

@Injectable()
export class UserService {
  findAll() {
    return ['user1', 'user2'];
  }
}

@Controller('users')
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Get()
  getUsers() {
    return this.userService.findAll();
  }
}
Enter fullscreen mode Exit fullscreen mode

Here, UserService is automatically injected into UserController.

➡️ Why it matters?

  • Makes testing easier (you can mock dependencies).
  • Encourages loose coupling.
  • Simplifies large codebases.

Think about ordering pizza 🍕. You don’t grow the wheat, make the cheese and cook the pizza yourself every time - instead, you depend on a pizza shop to give it to you.
That’s Dependency Injection in software. Instead of a class creating everything it needs by itself, NestJS provides the needed things (dependencies) for you.


3️⃣ DTOs, Serialization & the Getter Issue

When working with DTOs (Data Transfer Objects) in NestJS, you often want to control what fields are returned in API responses. This is where class-transformer decorators like @Expose() and @Exclude() come in.

🔹 @Expose()

Marks a property (or getter) to be included in the serialized response.

export class UserDto {
  @Expose()
  id: number;

  @Expose()
  name: string;
}
Enter fullscreen mode Exit fullscreen mode

➡ Only id and name will be serialized if you enable ClassSerializerInterceptor.

In NestJS, the ClassSerializerInterceptor cleans up API responses by hiding or exposing only the properties you mark (e.g., using @Exclude() or @Expose()). It’s mainly used to prevent sensitive fields like passwords from being sent back in responses.

🔹 @Exclude()

Removes sensitive/internal fields from the response.

export class UserDto {
  id: number;

  @Exclude()
  password: string;
}
Enter fullscreen mode Exit fullscreen mode

password will never appear in the response.

👉 Best practice: Use @Exclude() on class level, then selectively @Expose() only the fields you want returned.


⚠️ A Real-World Getter Issue

Imagine you want to dynamically compute a field. For example, a FolderDto that returns a folder type based on internal conditions:

export class FolderDto {
  isSpecial: boolean;

  @Expose()
  get type(): string {
    return this.isSpecial ? 'special' : 'default';
  }
}
Enter fullscreen mode Exit fullscreen mode

At first glance, this looks fine ✅.
But what if you later need to override type explicitly for certain cases?

dto.type = 'custom'; // ❌ Won’t work
Enter fullscreen mode Exit fullscreen mode

➡ The response will still show the getter’s computed value, because NestJS serialization prioritizes getters over explicitly set fields.


✅ The Robust Solution: Backing Property + Setter

To handle both cases (computed and explicit), use a private backing property with a setter:

export class FolderDto {
  isSpecial: boolean;
  private _customType?: string;

  set customType(value: string) {
    this._customType = value;
  }

  @Expose()
  get type(): string {
    if (this._customType) return this._customType;

    return this.isSpecial ? 'special' : 'default';
  }
}
Enter fullscreen mode Exit fullscreen mode

Now:

  • If you set dto.customType = "archive", the response will use "archive".
  • If you don’t set it, the getter’s logic (special/default) will apply.

👉 This approach is flexible and future-proof – you avoid being locked into only getter-based logic.


4️⃣ Other Useful NestJS Concepts & Best Practices

🔹 Pipes

Transform and validate input data automatically. Example: ValidationPipe for DTO validation.

🔹 Interceptors

Modify requests/responses, add logging, or handle caching. Example: ClassSerializerInterceptor to apply @Expose and @Exclude.

🔹 Guards

Control access with authorization logic (CanActivate). Example: AuthGuard for JWT.

🔹 Filters

Handle exceptions globally or locally. Example: HttpExceptionFilter.


🎯 Final Thoughts

NestJS offers powerful abstractions that make backend development faster, safer and more maintainable.

  • ✅ Use decorators to keep code clean.
  • ✅ Rely on dependency injection for flexibility.
  • ✅ Understand @Expose() / @Exclude() to manage responses.
  • ✅ Watch out for getter pitfalls—fix with setters and backing properties.

💡 By learning these patterns and best practices, you’ll avoid debugging headaches and build APIs that scale.


Top comments (0)