Introduction to SOLID Principles in Nest.js
So, if you're a NestJS or a TypeScript developer and you're not following these five SOLID principles, then you're probably writing bad code. So in this particular video, we're going to see what are the five SOLID principles that you should follow to write a cleaner and easier to maintain and easier to read code and to level up your coding skills from a junior developer into a senior developer. So we're going to see how you can implement the five SOLID principles to write cleaner and easier to read and easier to maintain code in TypeScript and in NestJS.
So if you're coming to this video, you probably already know about the other video I made about SOLID principles in React. If you don't know already, it's actually down below in the description. Go ahead and check it out to learn how to apply SOLID principles inside of your React applications. So we're going to employ SOLID principles in NestJS, the NestJS framework. And if you're not familiar with the NestJS framework, it's a really awesome, like REST, GraphQL kind of like framework that is built on top of Express.js, of course, on the Node.js ecosystem. And it has like more than 2 million downloads per week, which is super crazy. And the awesome part about this one is it actually uses TypeScript behind the scenes to handle everything. So we're going to have all the features of TypeScript, from like interfaces, types, classes, abstract classes, and so much more. And I'm going to use that to apply the SOLID principles for writing cleaner and better code.
S: The Single Responsibility Principle
Alright, so the first principle for us in here is the Single Responsibility Principle. And this one actually aims to have each class or module in the NestJS kind of application or a TypeScript application to have and be responsible only for doing a single task or functionality.
So for example, for us in here, we have an application that has like emails, orders, and products. So what we have in here, we have inside of the orders in here, inside of the controller, what you can basically do is actually submit an order and actually have people to basically, once they submit their order, I want to send like an email, like a confirmation email or order confirmation email to their email addresses saying, 'Oh, thank you for ordering from us. Here's actually the tracking number' or some information after, you know, submitting and confirming the order.
So inside of an order controller here, we simply have all, we just use it in here, we're injecting both the order service and the email service. And we go in here, we have this handler, which is a post handler that handles when submitting an order. It takes a body, so it takes the data of the order that we want to submit and so on and so forth. Now what we're doing in here, we're basically going to do like orderService.submitOrder(). So we're just delegating that into the service because it's actually the service that is responsible for handling orders, which is called the OrderService. And we're simply in here, just like connecting it to a database and do a bunch of stuff in here just to, you know, provide it with everything. So we have the products in here. Now let's try to look exactly what's going on inside the submitOrder.
Now here, what we're doing, just calling the Prisma order create, so we are just creating the order inside of the database in here to get the created order. Here particularly, there's actually a really bad approach. So here we are violating the Single Responsibility Principle. And that's just because we're using the email service, which is, you know, another service, the same service as we have in here, which is the OrderService. But what we're doing here, we're trying to send an email inside of the actual submitOrder inside of this service.
Now, this particular method in here should do only one single thing, which is submit an order. Like it does everything that is related to an order, we're like saving it to a database and processing it to save through the database, but it shouldn't actually take care of sending an email, right? Because if somebody else tries to come in around here and actually read our code, he's not going to understand exactly what we're doing here. We're trying to like call the sendOrderEmail in here inside of the submitOrder, where this particular one, the OrderService in here should only do stuff that is related to the order. And particularly, this method should only submit the order.
And yes, NestJS kind of like separates the concerns already by providing a controller, and module, and a service right over here. And of course, services should only do one thing. On the contrary, on the other hand, if you go back to the controller, actually, the right place to call and send an order email is actually inside of the controller. Why? Because the controller is actually a little bit of like, has a bigger scope compared to a service. If I'm here forward, you can just go and send an email or something after, of course, like you know, submitting an order, creating it in the database, getting the created order, and you can use the ID and so on and so forth, and eventually, you can just simply return a response. And that's going to give the opportunity to our service in here to be reused across the application because this service can be reused basically anywhere else. That's actually a service can be imported by another module that needs like the OrderService. And just by using submitOrder, it just like knows exactly that's going to only create it to the database, but it shouldn't send an email. And that's to actually follow the Single Responsibility Principle.
So for example, if you go to Postman and try to submit an order with, like, you know, a product ID 2 or something, you click save, it says, 'Oh, thank you for submitting an order.' If you get back in here, as you can see, sending an email has been called, and we're just simply sending an email, but not inside the service, but outside from the actual controller.
O: The Open/Closed Principle
Secondly, we have the Open/Closed Principle. And that's actually one of the simplest principles, but it actually has a huge value when you actually apply it. So this actually tells you you want to design your code to be open for extension, but it must be closed for modification. I know it's a little bit harder to understand that, but actually using NestJS, because NestJS comes out of the box with dependency injection or DI, you basically, you know, are going to make this principle for you a lot easier. And actually, NestJS always, always kind of like enforces you to use the Open/Closed Principle behind the scenes by just enforcing using dependency injection.
So for the Open/Closed Principle in here, for example, we've got two modules. One, which is the order module, when you know the user can make an order and actually pay for the order. And we have actually created another folder in here which has another module and another service, and this one should take care of like the payment stuff. So when the user actually, you know, submitted an order and comes, you know, to the checkout and tries to pay, he can just go ahead and delegate that into the payment service and actually use it.
Now inside of the payment service is actually where the magic happens. So this is actually the payment service. And let's imagine we've got a couple of payment services, right? Let's say we've got like a payment method, you know, you can use credit card, or you can use PayPal, or Bitcoin, or maybe you want to add later on like Apple Pay, so much more. So here we're simply doing just having a simple method. We say, 'Oh, process payment.' I'm just, you know, prepending in here 'bad' because it's actually the bad approach, and you can clearly see it on the top in here. But here where you can give it, you can give it the actual order in here, and you can give it the payment method, and it tries to do either a switch statement or an if-else statement, and it tries to do that depending on the payment method.
But why is this one actually bad? Simply because this code in here, whenever you try to do something like extend that code, like add another, let's say you want to add Apple Pay for this one. So for example, you want to add Apple Pay, you have to do an if statement. You do paymentMethod equals, you know, 'apple pay', whatever, and you have to do all the bunch of stuff in here. So you have to actually go inside and modify the code. And this code is not only for the Apple Pay, the code you're going to modify in here can touch pretty much all the payment methods, and that kind of violates how the Open/Closed Principle works because it should be closed for modification but open for extension.
Now what you should do instead to basically follow the Open/Closed Principle, you can go in and create another file, for example, payment.gateway.typescript. And inside of this particular file, we can just define all the gateways we have. So we can have like an abstract class, and here, abstract classes are very important, and because you're using TypeScript, it just makes it a lot easier. So we can do a PaymentGateway. This one is going to have like an abstract method called processPayment. And here we can just basically go ahead and use the abstract class PaymentGateway, and we can just go and implement it into like different payment methods, from like credit card to PayPal to Bitcoin. And let's say if you want to add this one, we can simply just go ahead and extend the code without modifying the code. So we're not touching the code we have already made, but instead, what we're doing is just basically go ahead and creating another gateway. So we're just extending that, we're implementing that PaymentGateway abstract class. I can here you can just go in and do, you know, the logic or the code to process the payment.
And another method that is going to actually process the payment. So you give it the order, you give it the payment method, and that's it. You just like give it that one, just go in and look into the payment gateways and actually it's going to go ahead and process the payment using the abstract class method in here we're implementing throughout all the payment gateways very easily. And that's just basically to follow the Open/Closed Principle. So you're like extending the code but not modifying the code.
L: The Liskov Substitution Principle
So the third one is Liskov Substitution Principle. So the principle basically states that, oh, you have to ensure that subclasses or derived classes can be substituted for their base classes without affecting the correctness of the program. So for example, in here for the Liskov principle in here, let's say we've got like a pricing and orders, and this pricing module in here is going to allow you to actually tell the pricing of the current orders and how it goes like depending if you have like a discount or promotion or something like that.
And here we've got this PricingService simply, which is an abstract class in here because we can implement it in different strategies, like have different ways, for example, one for a promotion, one for regular pricing, one for like, you know, sales pricing and so much more. And here we have this particular method called calculatePrice, you give it the base price, and it just returns that for you. So immediately you buy that, you know, oh, this one basically returns for you like the base price immediately. So this is basically not doing any modifications or whatever you give it, it just gives you that back.
And here we define two pricing strategies. So we have the regular pricing strategy and we have the sale pricing strategy. So if you jump to the regular one in here, we got two approaches, the bad approach and the good approach. For the bad approach in here, what happened in here, we're using extends instead of implements. While we're doing that, it's actually a really bad approach in here because that kind of like using extends instead of implementing in TypeScript kind of like violates how the Liskov Substitution Principle is. So simply because we're using extending here, and because the PricingService has already a default method that actually does the calculation and everything, TypeScript doesn't tell us, 'Oh, you need to go ahead and implement that.' And that's actually a huge mistake because the regular in here should have like a different pricing from, you know, the PricingService itself.
How you should do it instead is basically go ahead and use implements. And because when you use implements in here, you have to implement every single method, because if you don't do it, TypeScript is just going to basically just go and complain and say, 'Oh, because you're implementing that, you have to go ahead and define the method.' So you have to do calculatePrice in here, you have to, you know, basically return that, or you can just do whatever you want with it. And for example, for sale on here, that's the same thing. You can just, you know, the bad approach in here, you never want to use extends because that's pretty bad. For Sale, use implements and you can implement your own kind of like calculated pricing in here. You have that logic, for example, here you can have like a 20% discount. So doing it this way, we can go ahead and simply just, you know, follow the Liskov Substitution Principle in here.
For example, in here we got like the pricing service, so we do private pricingService and we just put the PricingService in here. And here we're injecting a particular pricing service. Let's say right now for this particular season, because we have no promotions or something, I just want to go ahead and use a RegularPricingStrategy in here. Just as simple as that. And I can just inject that. And because this PricingService is basically the parent of this one, so you can just interchangeably use that without any issues. And here we're using the calculatePrice in here, which is the same method, it can just work fine. So if I go back to Postman and try to give the price, and here you can see in here it tells us the regular price which is has no discounts or promotions for a hundred dollars.
But let's say we came to, you know, the promotion season, the Black Friday or something, I want to change my pricing strategy in here simply with another one. So I can just easily swap it out without doing or changing any code or modifying any code. So I can simply just go in and use the SalePricingStrategy. Right now, I can just inject it. And because, you know, it's part of the PricingService, I have to change nothing. It implements the same method, I have to change nothing. So that's pretty good. If I save that, I actually have my server restarted. I go back to Postman in here, and I try to do the same thing, as you can see, you have it now like a 20% discount. That's as simple as that. With a single line of change, I can use different pricing strategies. And that's what the Liskov Substitution Principle is for.
I: The Interface Segregation Principle
For the fourth principle, we have the Interface Segregation Principle. The principle is simply about defining fine-grained interfaces that are tailored to the specific needs of the consumers. And of course, it is actually going to allow you to avoid creating large interfaces with unnecessary methods that will not be used, you know, by everything, but instead just splitting them into smaller interfaces so they can be reused and correctly used across, you know, different packages or modules.
So for example, in here for the Interface Segregation Principle, we got like another verification folder. That's all we have in here. And let's say for this notification here, we have a notification.interfaces.typescript. So here for the bad approach is actually going and actually making an interface. And actually for the Interface Segregation Principle, it's all about interfaces and how you define your interfaces correctly. So here, we're going to use TypeScript interfaces in our favor. So for example, here for the bad approach is not like separating the concerns.
So let's say here we have like an interface, a Notification interface that what we want to use this for is actually like, for example, for sending an email which only needs like the to and subject, for example, for SMS we need the phoneNumber and message, and for push notification, we need like the userId and the title. So just putting them together makes it super hard to read that one, of course, makes like using specific values and like using interfaces very hard because you're using data that you don't need most of the time.
So instead of doing it this way, what you should do, you need to split those into a smaller kind of interfaces. Each does one thing and does one work. For example, EmailNotification, we only need the email properties. The SMSNotification, we only need the SMS properties, and so on and so forth. And of course, because we don't need all of that data, what we have to introduce, we have to introduce like the optional TypeScript symbol in here to make them all optional because you don't know when you have to use that or not have to use that. It makes it very hard to read. But instead, this one, it just like gives you the correct required and you can just use it correctly and in a very, very clean way.
And for example, imagine you can use that inside of a service. You want to send an email for the bad way in here. You're going to basically use Notification here. And Notification, remember like it has pretty much everything in here. It doesn't only have the properties or the fields that are only related to the email, but instead, it has pretty much everything shaft out into that one. But instead, what you should do, simply just, you know, use those small interfaces that you created. And that's what the Interface Segregation Principle is all about.
D: The Dependency Inversion Principle
For the fifth and the last principle, we have the Dependency Inversion Principle. The principle is actually for depending on abstractions rather than concrete implementations. And this just basically allows you to have a loose coupling between your components and physically AIDS easier testing and maintenance inside of your code base.
So for dependency inversion principle, we have this like storage module in here. Let's imagine we have this controller where a user just basically gets sent like a get request, going to ask for a file and a file name in here. I'm going to go ahead inside of our storage. For example, we have, you know, we're using, let's say Amazon S3, or maybe you're using Google Cloud Storage, something like that, or maybe Azure, whatever type of implementation you have behind the scenes. So you go inside, and what you do is you grab the file name, and you go and call the storage service findAmazonS3File, and you actually give it a file name and return it. And yes, this is bad. This violates the Dependency Inversion Principle.
OK, what is that? So let's go inside of the storage service and go inside of the class particularly in here. So this is actually what we have here because this storage service should be independent from the actual concrete implementation. And here the concrete implementation is actually, you know, finding it from Amazon S3 or maybe the other function we have in here, oh, findGoogleCloudStorageFile. You know, you have this very small concrete implementation inside of the service itself. And this, of course, it just like gives you a very fine-grained kind of like details of how everything should be done. But instead, this should be abstracted.
How? We should go down in here and we should go in and create an abstract class. So that called, oh, abstract class StorageFetcher. And this one going to allow you to implement a specific one which called the findFile. You give it a file name and you return basically some, you know, the file content from that one. And using this StorageFetcher because it's an abstract class, so inside of in here, we can implement different fetchers, going from the S3Fetcher or the GoogleCloudStorageFetcher. So here we can just simply, for example, for the S3, we can do a class that implements the StorageFetcher. And because, you know, they share this same method in here, we can implement basically the same one. They have the same interface, they have the same method name in here, the findFile, they're returning the same thing. And here we can just, you know, have whatever kind of implementation of that because this one simply is related to like the S3. And the same thing goes with the Google Cloud Storage fetcher. So you can have different ones. If you want to add another one, you can simply go ahead and add another fetcher, another service in here. You can simply inject it. You can have whatever implementation inside of it done.
Now inside our storage module in here, what we can do, we can go like, for example, 'Oh, I'm going to use this syntax for the providers where you can provide a class, which is, you know, the abstract class, which is our like StorageFetcher. Remember, this is the abstract class we have. And I can tell it to use a particular class, either to use the GoogleCloudStorage or the S3Storage. Let's say I wanted to use the S3Storage in here. So I can get back and I can go to my controller. So inside of a controller, you can use like the StorageFetcher in here. Simply I can do the storage, and I can just simply go ahead and use findFile. And that's it. And if you ever want to swap between those, inside of the module itself, I can just go ahead and change between the S3 kind of implementation into the Google Cloud implementation. And that's kind of like inverts the implementation or the dependencies from going to, you know, depending on the actual service and putting everything inside of the service, extracting it into their own kind of like services and fetchers to handle the data instead of like putting it all together in here. And that's for following the Dependency Inversion Principle.
Conclusion
Alright, thanks for watching. Hope you guys enjoyed these SOLID principles in NestJS and TypeScript. And catch you hopefully in the next ones.