π What is the Null Object Pattern?
The Null Object Pattern is a behavioral design pattern where:
- Instead of returning
null(which can cause errors if used without checking), - You return a special object that does nothing but still behaves like a real object.
β
This avoids if (obj != null) checks everywhere in your code
β
It helps follow Open/Closed Principle (extend behavior without changing logic)
π¨ Real-World Examples (Simple to Visualize)
π€ 1. Unknown User (Guest)
-
Instead of: Returning
nullfor unauthenticated users -
Do this: Return a
GuestUserobject that has safe defaults (no login, read-only)
π§ 2. Empty Cart
-
Instead of:
if (cart) { cart.checkout() } -
Do this: Use
EmptyCartobject with.checkout()method that logs "No items"
ποΈ 3. Logger
- A
ConsoleLoggerlogs to the console - A
NullLoggerjust does nothing (used in production or testing)
π§ Why Use It?
β
Avoid runtime errors from null
β
Reduce code clutter from if (obj !== null)
β
Improve readability & testability
β
Follow Polymorphism instead of conditionals
π§± TypeScript Example β Logger Pattern
Let's build a real use case: a logger.
1. Define Logger Interface
interface Logger {
log(message: string): void;
}
2. Real Logger (Console)
class ConsoleLogger implements Logger {
log(message: string): void {
console.log(`[LOG]: ${message}`);
}
}
3. Null Logger (does nothing)
class NullLogger implements Logger {
log(message: string): void {
// do nothing
}
}
4. Service Using Logger
class UserService {
constructor(private logger: Logger) {}
createUser(name: string) {
// ... create logic
this.logger.log(`User created: ${name}`);
}
}
5. Use It Without Null Check
const consoleLogger = new ConsoleLogger();
const nullLogger = new NullLogger();
const service1 = new UserService(consoleLogger);
service1.createUser("Alice"); // logs: User created: Alice
const service2 = new UserService(nullLogger);
service2.createUser("Bob"); // logs nothing, no error
β
No need to check if (logger != null) β just inject the correct behavior.
π‘ Better Than Null Check
Instead of:
if (logger) {
logger.log("something");
}
With Null Object:
logger.log("something"); // always safe!
π Real-World Use Cases
| Context | Null Object |
|---|---|
| Logging |
NullLogger (test env, prod) |
| Payment |
NullPaymentProcessor (free plan) |
| User |
GuestUser (not signed in) |
| Cart |
EmptyCart (user hasn't added anything) |
| Strategy |
NoOpStrategy for default behavior |
π― Pro Tips for Mid-to-Senior Devs
β
Replace null/undefined returns with Null Object classes
β
Let Null Object implement the same interface
β
Makes code cleaner, testable, and follows polymorphic behavior
π§ͺ Testability Bonus
In unit tests, instead of mocking a logger:
const service = new UserService(new NullLogger());
No mocks needed β just use the Null Object!
π Final Summary
"The Null Object Pattern replaces null with an object that safely does nothing β reducing checks and avoiding errors."
Perfect! Letβs now apply the Null Object Pattern in a NestJS service layer β a common place where it adds a lot of clarity and safety. Iβll walk you through a practical example slowly and clearly.
β Use Case: Optional Logger in a NestJS Service
Letβs say you have a UserService that logs events (like user creation), but sometimes you donβt want logging (e.g., during tests or in certain environments). You donβt want to check if (logger) everywhere.
π§± Step-by-Step: Null Object Pattern in NestJS
πΉ 1. Create a Logger Interface
// logger/logger.interface.ts
export interface LoggerService {
log(message: string): void;
}
πΉ 2. Real Logger Implementation
// logger/console-logger.service.ts
import { Injectable } from '@nestjs/common';
import { LoggerService } from './logger.interface';
@Injectable()
export class ConsoleLoggerService implements LoggerService {
log(message: string): void {
console.log(`[LOG] ${message}`);
}
}
πΉ 3. Null Logger (does nothing)
// logger/null-logger.service.ts
import { Injectable } from '@nestjs/common';
import { LoggerService } from './logger.interface';
@Injectable()
export class NullLoggerService implements LoggerService {
log(message: string): void {
// do nothing
}
}
πΉ 4. Inject Logger into Your Service
// user/user.service.ts
import { Injectable } from '@nestjs/common';
import { LoggerService } from '../logger/logger.interface';
@Injectable()
export class UserService {
constructor(private readonly logger: LoggerService) {}
createUser(username: string) {
// ... your real user creation logic
this.logger.log(`User created: ${username}`);
}
}
πΉ 5. Provide Either Logger in Your Module
// app.module.ts
import { Module } from '@nestjs/common';
import { UserService } from './user/user.service';
import { ConsoleLoggerService } from './logger/console-logger.service';
import { NullLoggerService } from './logger/null-logger.service';
import { LoggerService } from './logger/logger.interface';
@Module({
providers: [
UserService,
{
provide: LoggerService,
useClass:
process.env.NODE_ENV === 'test' ? NullLoggerService : ConsoleLoggerService,
},
],
})
export class AppModule {}
β Now:
- In test env, it uses
NullLoggerService(no logs, no noise) - In prod/dev, it uses
ConsoleLoggerService(full logs)
π― Why This Works Well
| Benefit | How It Helps |
|---|---|
β
Avoid null checks |
No if (logger) needed anywhere |
| β Clean DI | Swap behaviors easily at runtime |
| β Safe by default |
NullLogger never throws |
| β Open/Closed Principle | Add new loggers without changing service logic |
| β Testing-friendly | Inject NullLoggerService in tests, no mocking needed |
π§ͺ Test Usage (Easy)
const module = await Test.createTestingModule({
providers: [
UserService,
{ provide: LoggerService, useClass: NullLoggerService },
],
}).compile();
const service = module.get(UserService);
service.createUser('Alice'); // runs silently
π§ You Can Extend This Pattern To:
-
AnalyticsService β
NullAnalyticsService -
EmailService β
NullEmailService -
CacheService β
NullCacheServicefor dev mode -
NotificationService β
NullNotificationService
π§΅ Final Summary
In NestJS, use the Null Object Pattern by creating default "no-op" services that follow the same interface as real ones β this simplifies logic, removes conditionals, and improves testing and runtime flexibility.
Top comments (0)