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();
}
}
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;
}
➡ 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;
}
➡ 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';
}
}
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
➡ 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';
}
}
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)