📖 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
null
for unauthenticated users -
Do this: Return a
GuestUser
object that has safe defaults (no login, read-only)
🧃 2. Empty Cart
-
Instead of:
if (cart) { cart.checkout() }
-
Do this: Use
EmptyCart
object with.checkout()
method that logs "No items"
🗃️ 3. Logger
- A
ConsoleLogger
logs to the console - A
NullLogger
just 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 →
NullCacheService
for 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.