Project Overview and Creation
Hey there. Today we're going to take a look at NestJS microservices. We're going to set up a sample application that includes an API gateway that our client can talk to, and then we're going to connect two separate microservices to this gateway. So we're going to need three separate NestJS projects for this.
So in your command line, in whichever folder that you'd like to work in, make sure you run nest new and then we'll start with our API gateway. So this will be a sample-backend, that's what we'll call it.
And I will include the link to the GitHub repo for this project in the description as always, as well as a link to the microservices docs. So once that's done, we're going to do nest new, and then now we're going to include our first microservice, which we'll call sample-communication.
And this microservice will represent some sort of communications-based service that would email or text or really any form of communication that we want to handle to our users. And the last project we'll create: nest new sample-analytics, so this will be a microservice that handles analytics for our application.
Workspace and Communication Microservice Setup
So once we finish initializing our projects, I've opened the sample-backend up in VS Code and I'm going to actually go to File and then Add Folder to Workspace. And then I'm going to go ahead and add the sample-analytics, and then I'll do the same thing for the sample-communication. So now all three of our projects are in one VS Code project, and then we can save this workspace.
So first off, let's go ahead and set up our analytics service to connect as a microservice... So to start off, let's go ahead and set up our sample-communication project as a microservice. So our first thing we need to do is open up the terminal for sample-communication and make sure we add yarn add @nestjs/microservices.
Now, NestJS offers us a number of different transports that we can use to connect our microservices. By default, the transport is TCP, which is what we're going to use. So for this microservice, we're not going to be listening for any HTTP connections; we're not going to expose it to the outside world. In the other microservice, I'll show you how we can do both, but for now, we're only going to want to set up a TCP microservice that will be listening for events and messages from other services.
So we'll create an app here, and then we'll call await NestFactory.createMicroservice. We're going to pass in microservice options here. And now we need to pass in the AppModule as we normally would, and then a set of options. In this case, we're going to specify the transport that we're going to use. And here you can see the list of different transports that are accepted. So we're going to use TCP for this example. And then lastly, we need to call app.listen().
And that's all we need to do to set this microservice up.
Configuring the API Gateway Client
So now that we've connected our sample-communication project, let's go back up to our sample-backend, which is acting as our API gateway. And by that, I mean this is the actual project that will receive API requests from clients and then dispatch different events and messages to these different microservices. This way, we really only need to expose one central API, or an API gateway, to our clients.
Now, just as we've done before, we're going to open up a terminal for the sample-backend, and we'll have to add the microservices package from NestJS. Now, the main.ts will stay as the same as it always is, but now in the AppModule, what we're going to do is in the imports section, we need to register something called the ClientsModule from NestJS microservices. And we're going to call register on it. And what this is going to do is it's going to allow us to inject a number of services that our actual application can use to dispatch events and messages to the microservices.
So let's see this in action. For example, let's hook up the communication service that we just created. So we can provide any name. This can be any name, that all this will represent is the injection token that we'll use later on. So in this case, I'm calling it 'COMMUNICATION' to represent the communication service. Next, we need to specify the transport. So we know in this case the transport is TCP. And now we can actually use this in our app code to communicate with our services.
Implementing the Event-Based Pattern
So let's do just that. In our AppController, I'm going to actually create a new route here. And this will be a POST route, and we're going to call it create-user. So we're going to take in a create user body. Let's quickly just create a CreateUserRequest DTO. And we can export a class here, CreateUserRequest, and we're going to have an email and password that come in over the network as two strings. So now in our AppController we can specify the body here. We know it'll be the createUserRequest of type CreateUserRequest.
So now that we have this DTO, we can specify to our AppService that we want to handle this. So in our AppService, we're going to go ahead and have a createUser method. It's also going to take the CreateUserRequest.
In our AppService, just to make things simple, we can have an in-memory database here. So I'll have an in-memory array of users, type any. So this will be an empty array here. And simply we can just push the createUserRequest to this array. So basically, we are just allowing the API request to come in and add to this array. Pretty basic stuff.
But now what I want to do is emit an event to our communication service, telling it that the user has been created, so that it can go ahead and handle different actions like maybe sending an email to the user, sending them a text message, allowing them to subscribe—any number of concerns that really are beyond simple CRUD operations and we want to keep clean and separate.
So to do this, we're going to use that injection token we created earlier. So we're going to call Inject here and specify that injection token. So we know it's the 'COMMUNICATION' string that we specified. And this will be private readonly, we can call it the communicationClient, and this is going to be of type ClientProxy from NestJS microservices.
So now that we have this communicationClient, in createUser we can specify that we want to emit an event when the user was created. So we can specify a pattern here, which is really just a string literal that we'll specify the event that we're emitting. And then we can specify a payload, a data payload. So I'll go ahead and create a new event. I'm going to call this CreateUserEvent, and all this is going to be is just an object here that is really just going to take the email. We don't want to specify the password over the network. All we're concerned about is the user's email that was just created.
So we have this new event, and we can use it now. So specify a new CreateUserEvent, and then of course we'll pass in the email from the createUserRequest. So now after we're creating the user, we're emitting this event to our microservice. So in our AppController, make sure we actually call this.appService.createUser and pass in the request. And so now back in our communication microservice, we need to wire things up.
Handling Events and Testing the Flow
So our controller acts as the entry point for all communication in the app, and it will be the same for events and messages in this architecture. So we'll add a decorator here called @EventPattern, and now we specify the pattern to which we want to listen for. And of course, we know this is the user_created event pattern. And then we can say we want to handleUserCreated. So this is the event that we are responding to, and we know that we're going to get a data payload here, and this is going to be the same CreateUserEvent.
And we can simply copy over the other event we've created and paste it in here because we know the payload will be the same. So we can say a CreateUserEvent is what's being transferred over the wire. And now we have access to this event.
So our AppService will create another method called handleUserCreated which will take the data. And of course, you know this is the CreateUserEvent. And now in here, let's simply just log out in here saying, handleUserCreated method in the communication service, and we'll log out the data. And maybe we could add a to-do here for later in the future, or if it's something you'd want to implement, we could email the user and welcome them to our service.
So in the AppController, we'll have to make sure we call handleUserCreated and pass in the data.
So now back in our terminal, you want to make sure we're running yarn start:dev in both the sample-backend so we can listen for API requests, and do the same for the sample-communication app. Now once those have both started up, we can open up Postman and we can make a POST request to http://localhost:3000 which is where our API gateway is listening for. And our default POST route is the create-user route. So we can specify that body with the email and password and send off a few different requests here, changing the email and password. And then if we go take a look at our communication service, we can see that it is correctly logging out for each request that comes into our backend. We're sending the handleUserCreated event, and the communication service is correctly responding. You can see it log out the event here.
Creating a Hybrid Microservice
So nice work with that basic example. Let's go ahead and now set up our last microservice, the analytics service. So this will be a hybrid application that is a microservice and it will also listen for HTTP requests.
So as we've done before, let's open up the sample-analytics project, and of course we'll have to add @nestjs/microservices. Once that finishes, we can open up the main.ts. Now this main.ts file setup will be a bit different because we are actually going to keep this bit of code here where we create the Nest factory and we listen. However, I'm going to change this port to 3001, because our sample-backend is already listening on port 3000, so we don't want those to conflict.
And now what we're actually going to do is we can call app.connectMicroservice, and here we can specify the transport. This will be Transport.TCP. And additionally, we need to specify an options object here, and this is because we already have our communication service listening on port 3000, so we don't want those to conflict either. We'll have to specify a separate port here. So now we're going to tell it to listen on port 3001 for this microservice. So now in addition to calling app.listen(), we need to call await app.startAllMicroservices().
So now we've created a hybrid application that both has a microservice connected and it's listening for HTTP requests.
Implementing a Multi-Client Event Flow
Now in order to make use of the request-response based microservice communication pattern, I want to set up another event pattern here that also responds to user created events. So as we've done before, we can simply specify the event pattern and say that we want to respond to user_created events in our app controller here. So this will look very similar to the method we've already created. We'll have a piece of data, and we'll have to specify the CreateUserEvent. Make sure we paste this in, the CreateUserEvent with just the email. And back in the AppController we can use this here, the CreateUserEvent.
And now in our AppService, let's go implement this. And so instead of just logging things out, we're actually going to maintain an array here that we can use for our analytics and our request-response pattern. So I'm going to declare a readonly array here called analytics, and it'll be of type any and set it to an empty array. So now in our handleUserCreated method, we'll take in the data. We know it's the CreateUserEvent, and we can of course still log out as we've done before to make sure things are working as expected. But then I also want to push to our analytics array. I want to push an object where we'll include the email from the request, but also I want to just include the timestamp. So this will maybe be some fake analytics data. We know exactly when the user was created, and maybe we could send this to some sort of external server, anything you would do to maintain some analytics.
So now in here we will reach out to the AppService and pass in the data.
So as we've done before, we're going to need to hook this up into our API gateway. So open up the AppModule in the sample-backend again, and as we've done before, we'll have to specify a different object here in the ClientsModule. So in this case, we'll have a new name for our injection token. I'll call it 'ANALYTICS'. Of course, we'll specify the transport, which we know is TCP, and now since the port is different, we'll specify an options object and specify the port at 3001.
So in the AppService, let's go ahead and paste this same line here. We'll have to change this ClientProxy to be of type 'ANALYTICS' here for our injection token, and then of course change the name. We'll call this analyticsClient, and in createUser event, I want to now emit two events: one to the communicationClient and one to the analyticsClient.
So finally, as we've done with our other microservice, make sure you cd into the sample-analytics folder and run yarn start:dev to start this service up. And now if we fire off another create user request, we should see our analytics handler log out the payload here, just as we do in the communication service.
Implementing the Request-Response Pattern
So finally, let's go ahead and implement an example with a request and a response to our services. So in our sample-backend, what I want to do is create a new route here. This is going to be a simple GET route with the name 'analytics'. We are going to call this getAnalytics. So we're going to proxy this off to our analytics service. So in our AppService, I'll add a new method here called getAnalytics, and all this is going to do is proxy off to our analyticsClient, and we're going to send a command. So this is going to be similar to our event. It's going to be an object which will be matched to whichever service we send to.
So we'll add an object with a cmd key and we'll just simply call this get_analytics. And the payload will be an empty object. We don't need to include any data for this request.
Now, importantly, this request here, the send method, it returns what's known as a cold observable. So only when your code subscribes to this call will the message actually get sent. Thankfully, NestJS, when you return an observable from a controller route, it's automatically subscribed to and the result is returned to the client. So we can simply call getAnalytics from our controller here and everything should work as expected.
So let's go back into our analytics controller. And as we've done with our event pattern, we're going to need to specify a new decorator here called @MessagePattern, where instead of just a string, we're going to specify the whole pattern. So this is the object with the cmd, and of course we just set this up, it's called get_analytics. So we can now actually implement this method, which has been proxied on from our backend API gateway, and we will call this.appService and implement a simple method in here. We'll call it getAnalytics, and all this is going to do is return this.analytics. Very simple. So we can just call getAnalytics.
Final Demonstration and Conclusion
And so now after we go ahead and create a few more users, hopefully see that the analytics service is being updating the analytics array. So now if we call our backend with the analytics route and call GET, we can see this data that has been returned from the analytics service, the list of emails with the timestamp.
And additionally, our analytics service also has an HTTP server. So let's make sure that's working okay. On port 3001 if we simply call the normal GET route, we can see the basic HTTP response, "Hello World!".
So this has been a very basic example of NestJS microservices. For my next video, I'm going to show you how you can deploy a NestJS application using Kubernetes. So if you'd like to see that, make sure you subscribe, like the video, and leave a comment if you have any questions. And I'll see you in the next one.