Introduction & Project Setup
Hey there. Today we're going to take a look at events and task scheduling in sjs which can help us speed our application up and decrease coupling, which are both things we want when developing a backend service.
So I'm in a new directory here and I'm going to run nest new nestjs-events-tasks to create a new project, and I'll use pnpm as my package manager. So after our project is finished installing, we'll go ahead and cd into that project and we're going to have to install a few packages.
The first one is going to be nestjs/event-emitter. So go ahead and install this package. And then for task scheduling, we're going to have to install nestjs/schedule. And then finally, we're going to install the types for the cron package. So once we have all these packages installed, we can go ahead and run pnpm start:dev to go ahead and start our development server.
Module and Controller Configuration
So now that we're inside of our project in VS Code, I'm going to go ahead and open up the app module and in our imports array, we're going to have to set up the event emitter and the scheduler module. So first for the event emitter module, we'll import this from sjs event emitter and then call forRoot to initialize it. And then we'll also set up the schedule module forRoot from nestjs schedule. Now, both of these modules here can take an options object for further configuration. I'm going to leave the defaults for now because out of the box they work fine, but I'll leave a link in the description to where you can learn more about these options.
So now that we've imported these modules, let's go ahead and open up the app.controller and in here, let's go ahead and create a new post route where we can make a mock call to create a user in our system. So here we'll get the body from the request. And so we'll annotate this with body from sjs common.
And then we want to provide the type for the body here. So let's quickly go ahead and create a dto folder in our source directory, and then I'll create a new file called create-user.request.ts. And so we'll just simply export a CreateUserRequest in here that takes the email of the new user we want to create as well as the password, which are both strings. So now back in our app controller, our body we can now say is a type CreateUserRequest, and we'll provide a return type of promise void here. So then we're going to offload this call to our app service and simply say createUser where we pass in the body. So now let's actually go into our app.service and we can start playing around with the event emitter and the scheduler.
Emitting Events on User Creation
So let's go ahead and create our async createUser function here, which we know takes the body of type CreateUserRequest. And the first thing I want to do is log a message in here just to say that we're creating the user. So let's create a new logger up top. We'll say logger is equal to new Logger from sjs common and we'll just pass in the AppService.name to give some context to these logs. So in here we'll say this.logger.log and we'll just simply say "Creating user" and maybe we'll provide the body so we can actually see what's getting sent to the service.
So now in Postman, if I send off a POST request to our localhost:3000 server with a body of email and password, we send this off and then look at our logs, we can see our "Creating user" log statement with the body that we're sending. So now in our createUser function, let's say after we create a new user, we want to do a few actions. The first of which is we want to send them an email and then we want to send them a welcome gift. Now, typically what you could do is you could do both of these actions in the createUser function. However, the issue with this is that both of these actions might take a good amount of time, and the client which requested to create the user doesn't really care about waiting for the email to be sent and the welcome gift to be sent. All it cares about is getting a response back that says that the user was persisted, and no other asynchronous actions have to block us. So in order to decouple this functionality to the point where all we do in here is create the user and then we do these other actions, we can use events and we can use the event emitter.
So let's go ahead and see this in practice. I'll inject the event emitter up top which is going to be of type EventEmitter2. So in createUser, this is where we've created the user and you could pretend that we've persisted to the database and now we want to return to the client to return as quickly as possible. However, before we do that, let's send off the user created event so that additional functionality can be accomplished. So we'll call this.eventEmitter.emit. Now emit takes the name of the event that we want to emit. So this can be any string, so we'll call user.created as the event name. And then we can provide the payload, which is any data that will be passed along to the functions that are listening to this event. So I want to pass a UserCreatedEvent in here. To do that, I'll create a new events folder and then we'll create a user-created.event.ts. And this will be a class that will just hold some information about the user created event. So I'm going to create a constructor where we get the user ID that was just created as well as the email. So now we have both of these pieces of information.
We can create our UserCreatedEvent. Now I want to pass in a fake user ID, so I'll just create one up here and set it to one, two, three. So now in the constructor call, we'll pass in the user ID and then we'll pass in the email from the body. So now after we created the user, we simply fired off this user created event and we returned back to the client without waiting for whatever this event causes.
Handling Events with Listeners
Now I actually want to react to this event and do some work in a separate thread so that we don't block the response to the client. So what we'll do is we'll create a function decorated with the @OnEvent decorator where we specify the event we want to listen to. So it's as simple as that. We just pull user.created which is the same name we emitted earlier, and then we'll say welcomeNewUser that takes in a payload of the UserCreatedEvent. And in here, this is where you could potentially send an email to a new user. We'll simply log out a message that says "Welcoming new user" and we can provide the payload.email.
So now back in Postman, if we send off a new request to create a user, we should see in our logs that we did create the user, "creating the user" and here's the body, but then we also have our event handler here where we welcome the new user and provide the email that's coming from this payload.
Now we can have multiple event handlers listening for the same event. So if we have one event handler that's welcoming the new user, we can have another one that will send a gift to this new user. So let's say we have an async sendWelcomeGift function. It's going to get the same payload, UserCreatedEvent, and because this function is asynchronous, we want to actually provide that in the decorator here so that it's called properly. So we'll say async is set to true. And now we could do some async work inside of this function. So I'll call this.logger.log and we'll say "Sending welcome gift". I'll of course provide that email again in the log statement. Now let's simulate some sort of asynchronous delay where we would be sending the welcome gift. So to do this, I'll just await a new promise here where all we do is just wait using setTimeout and after a certain amount of time, we'll resolve this promise and we'll say we want this to happen after three seconds, so 3000 milliseconds. And after this asynchronous action, we'll call this.logger.log, "welcome gift sent" where we provide the payload.email again.
So now back in Postman, let's quickly send a request off. Go ahead and look at our logs, we can see a bit of a delay there where we saw sending welcome gift and then the welcome gift was sent, in addition to the other event handler where we welcomed the new user. So we can really see the power of these events in that it allows us to decouple functionality in our application, and all asynchronous work can be handled in separate threads, so to say.
Static Task Scheduling with Cron Jobs
So now that we've looked at events, let's take a look at scheduling tasks in sjs. So Nest with their schedule module makes this very easy. Let's go ahead and take advantage of a decorator from this package, the @Cron decorator. So the cron decorator is super powerful. It uses a special pattern that allows you to provide a string representation of when you want events to occur. And I will actually include a link in the description to where you can read more about this string pattern.
However, out of the box, Nest also exports CronExpression, which is a number of useful predefined patterns that lets you execute code every certain interval. So in our case, let's go ahead and use EVERY_10_SECONDS and you can see all the patterns here. They get very specific, every 30 seconds, between 9 and 6 and so forth. We want every 10 seconds. So every 10 seconds, this bit of code will be executed. And we'll say we will deleteExpiredUsers every 10 seconds. So in the logger, we'll simply log "Deleting expired users".
So now back in our terminal, we should see every 10 seconds a log statement saying "deleting expired users", which would represent some sort of asynchronous action every 10 seconds. So here we go, the first log statement, and this will continue every 10 seconds until your application stops.
Dynamic Task Scheduling with SchedulerRegistry
Now, this is a very useful way if you know ahead of time you have certain tasks that you want to execute, using the static cron decorator is a very good approach. However, some cases you want to trigger a certain code to occur dynamically, and in order to do that, we can use the SchedulerRegistry service here. So we'll call private schedulerRegistry which is of type SchedulerRegistry.
So with this scheduler registry, there's a whole lot that we can do. And if we take a look at the methods offered by it, schedulerRegistry., you can see a number of useful methods. So if you look at the methods here, we can see we can dynamically add cron jobs, intervals, and timeouts. Now, intervals are similar to cron jobs where they will execute code every amount of time you specify. And then the timeout is like setTimeout where it'll execute code once after a certain amount of time. And of course, it has corresponding methods to delete cron jobs, intervals and timeouts.
Now, you can actually specify the name of a cron job like we specified over here with the options object where you specify the name. So in this case, we'll have deleteExpiredUsers. And then you can use that dynamic SchedulerRegistry to even delete this cron job or add it back from this name here. What we're going to do is after we emit this user created event, let's go ahead and say we want to establish a websocket connection after five seconds. After the user has been created, we want to establish a websocket connection.
So in order to do this, first, we'll need to create the timeout interval actually. So we'll get this in a variable and call it establishWebsocketTimeout. So this will be equal to setTimeout where we provide a function that we want to call. And in this case, I'll just create a simple private function below called establishWebsocketConnection that takes the user ID of type string. And this is where we would establish some sort of websocket connection with the user. In this case, we can just call this.logger.log and say "Establishing websocket connection with user" and pass in the user ID. So now in our setTimeout, we'll call this.establishWebsocketConnection, pass in our user ID, and then we can specify when we want this to occur. So let's say after five seconds, so 5000 milliseconds, we want this code to execute. Now to actually ensure that this setTimeout occurs even after we return from this function, we are going to use a SchedulerRegistry. We'll call schedulerRegistry.addTimeout. Now we'll provide the name of the timeout, so this is if you wanted to maybe delete it later on or check if it exists, you'll do so by this name. And we'll call it userId_establish_websocket so it'll be scoped to each user and they'll have their own timeout here. Lastly, we'll provide the setTimeout function we created above.
Now importantly, this scheduler registry is an in-memory store, it's not distributed. So if you have multiple instances of your app running, you'll need to keep that in mind. So let's go ahead and see this in action. Back in Postman, we should expect to see this websocket connection established five seconds after we send this request. So let's send it off and look in our logs. We can see our welcome gift was sent and there we see our websocket connection has been established with the user ID, and we can still see our cron job is running in the background.
Conclusion & Next Steps
So this has been a very brief introduction into events and task scheduling. They're both very powerful when used properly. In my next video, I will be walking through an end-to-end ultimate guide around Nest.js microservices. So if that's something you think you'd be interested in, make sure you subscribe. I'll see you in the next one.