I didn't plan to publish this; it was initially for my own reference. This blog complements the official Nestjs documentation.
Use Cases
Standalone Applications
These applications don't handle network traffic like HTTP or WebSocket connections but still use the NestJS IoC container, benefiting from NestJS's dependency injection features.
Ex. CLI Tools
HTTP Servers
To handle HTTP requests and responses.
Microservices
Microservices decompose the application into smaller, independent services that communicate with each other.
IOC and DI
IOC (Inversion of Control) and DI (Dependency Injection) are two core concepts in NestJs. Excerpt from IOC vs DI provides great insights into these two:
IOC (Inversion of Control) and DI (Dependency Injection) are related concepts in software development, but they are not the same thing.
IOC is a programming principle that is used to invert the flow of control in an application. Instead of the application being responsible for controlling the flow of execution, IOC allows the framework or container to take control. The framework provides the necessary infrastructure for the application to run, and the application uses this infrastructure to perform its functions.
DI, on the other hand, is a design pattern that is used to manage the dependencies between different components in an application. In DI, the dependencies are "injected" into a component by a container or framework, rather than the component itself creating or managing its own dependencies. This allows for greater flexibility and modularity in an application, as components can be easily swapped out or replaced without affecting the rest of the application.
In other words, IOC is a principle that allows for greater flexibility in how an application is executed, while DI is a design pattern that allows for greater flexibility in how an application is structured and managed.
In practice, IOC and DI are often used together, as DI is a common way to implement IOC. By using DI to manage dependencies, the framework or container can take control of the application's execution and provide the necessary infrastructure for the application to run.
Benefits of IoC
Loose Coupling: IoC promotes loose coupling by removing the direct dependencies between components, making the system more flexible and easier to maintain.
Testability: Since dependencies are injected from outside, it becomes easier to replace real implementations with mocks or stubs during testing.
Reusability: IoC allows components to be reused in different contexts because they depend on abstractions rather than concrete implementations.
IoC Techniques
NestJS uses Dependency Injection, which is the most common form of IoC.
// Without IoC (Traditional Control Flow)
class Engine {
start() {
console.log('Engine started');
}
}
class Car {
constructor() {
this.engine = new Engine(); // Car controls the creation of Engine
}
start() {
this.engine.start();
}
}
const myCar = new Car();
myCar.start();
// With IoC (Using Dependency Injection)
class Engine {
start() {
console.log('Engine started');
}
}
class Car {
constructor(engine) { // Engine is injected via constructor
this.engine = engine;
}
start() {
this.engine.start();
}
}
const myEngine = new Engine();
const myCar = new Car(myEngine); // Control of Engine creation is outside Car
myCar.start();
Programming Paradigms
- Object-Oriented Programming (OOP)
Encapsulation
As
users
is marked as private, no external code can directly access or modify it.By encapsulating the
users
array and providing controlled methods to access and modify it, theUsersService
class ensures that the internal state (i.e., the list of users) can only be altered in specific, predictable ways. This prevents the array from being modified arbitrarily, which could lead to bugs or inconsistent data.@Injectable() export class UsersService { // Private property (Encapsulation) private readonly users: User[] = []; // Public method to retrieve a user by ID (Controlled Access) getUser(id: number): User { return this.users.find(user => user.id === id); } // Public method to create a new user (Controlled Modification) createUser(createUserDto: CreateUserDto): User { const newUser = { id: Date.now(), ...createUserDto }; this.users.push(newUser); return newUser; } }
Inheritance
@Injectable() export class BaseService { createUser(name: string) { ... } getUser(id: number) { ... } }
BaseService: Contains common functionality for managing users (e.g.,
createUser
andgetUser
methods).@Injectable() export class UserService extends BaseService { // User-specific methods } @Injectable() export class AdminService extends BaseService { // Admin-specific method deleteUser(id: number) { ... } }
UserService: Inherits from
BaseService
and can use its methods.AdminService: Also inherits from
BaseService
, so it has access to the common methods. Additionally, it has an admin-specific method,deleteUser
.Using the Services in a Controller:
@Controller('users') export class UsersController { constructor( private readonly userService: UserService, private readonly adminService: AdminService, ) {} @Get(':id') getUser(@Param('id') id: number) { return this.userService.getUser(id); } @Post('admin/delete/:id') deleteUser(@Param('id') id: number) { return this.adminService.deleteUser(id); }
Polymorphism
Polymorphism enables objects of different classes to be treated as if they were objects of a common superclass. Polymorphism establishes a common interface (or supertype) that defines a set of methods and their signatures. Objects of different classes that implement this interface can then be used interchangeably, as the compiler or runtime environment will invoke the appropriate method based on the actual object's type.
💡We can also enhance above setup by implementingUserService
andAdminService
as instances ofBaseService
if we ever needed to handle them polymorphically.interface PaymentService { processPayment(amount: number): void; } @Injectable() export class StripePaymentService implements PaymentService { processPayment(amount: number): void { ... } } @Injectable() export class PayPalPaymentService implements PaymentService { processPayment(amount: number): void { ... } }
The
PaymentService
interface defines a contract that any implementing class must follow.StripePaymentService
andPayPalPaymentService
are two classes that implement thePaymentService
interface. Both classes provide their own version of theprocessPayment
method.@Injectable() export class CheckoutService { constructor(private readonly paymentService: PaymentService) {} checkout(amount: number): void { this.paymentService.processPayment(amount); } }
The
CheckoutService
class is designed to handle the checkout process. It does not know or care about the specifics of how the payment is processed. Instead, it relies on thePaymentService
interface. Because theCheckoutService
depends on thePaymentService
interface rather than a concrete class, it can work with any class that implements this interface.Based on the user's selection, the application determines which payment service to use. This could be done via an internal configuration or a switch-case logic that maps the selected payment method to a corresponding payment service.
let paymentService: PaymentService; switch (selectedPaymentMethod) { case 'PayPal': paymentService = new PayPalPaymentService(); break; case 'Stripe': paymentService = new StripePaymentService(); break; // Add more cases for other payment services default: throw new Error('Unsupported payment method'); }
The
CheckoutService
is instantiated with the selectedpaymentService
and calls theprocessPayment
method on the injectedpaymentService
, which processes the payment according to the specific logic of the chosen service.const checkoutService = new CheckoutService(paymentService); checkoutService.checkout(amount);
Dependency Injection
NestJS uses Dependency Injection for managing dependencies and promoting loose coupling.
Modular Programming
In NestJS each module encapsulates a set of related components. Modules can be imported into other modules, allowing for scalable and maintainable code organization.
Let's connect: