Events

AdonisJS has an inbuilt event emitter which is slightly different (for good) from the Node.js event emitter. By the end of this guide, you will know:

  • How to use the AdonisJS event emitter for emitting events.
  • Differences between AdonisJS and Node.js event emitter.
  • Making your events type safe.

Basic Example

Let's begin by creating a new file inside the start folder and preload it during the application boot process.

Create start/events.ts file

touch start/events.ts

Next, open the .adonisrc.json file and add it to the array of preloads. The files under the preloads array are automatically loaded by AdonisJS during the boot process.

{
  "preloads": [
    "./start/routes",
    "./start/kernel",
    "./start/events"  ],
}

Open start/events.ts inside your text editor and paste the following code snippet inside it.

start/events.ts
import Event from '@ioc:Adonis/Core/Event'

Event.on('new:user', (user) => {
  console.log(user)
})

The Event.on method register a new event listener that is invoked everytime the new:user event it fired. For demonstration, let's create a route that emits this event after handling the HTTP request.

start/routes.ts
import Route from '@ioc:Adonis/Core/Route'
import Event from '@ioc:Adonis/Core/Event'

Route.get('/register', async () => {
  // create user
  const user = { id: 1 }
  Event.emit('new:user', user)

  return 'User registered'
})

Now, if you visit http://localhost:3333/register, you must see the console.log statement executed by the event listener.

Making Events Type Safe

Since, the events listeners and the code emitting the events are not in the same file, it is very easy to make the mistake of sending wrong arguments to a specific event. To prevent this behavior, AdonisJS allows you to define the shape of data an event can/must receive.

Open the pre-existing file contracts/events.ts to define the shape of data for the new:user event.

contract/events.ts
declare module '@ioc:Adonis/Core/Event' {
  interface EventsList {
    'new:user': { id: number },
  }
}

Now, if you attempt to emit the id as a string to the new:user event, the TypeScript compiler will complain about it.

Dedicated Listener Classes

Just like controllers and middleware, you can also extract the inline event listeners to their own dedicated classes. Begin by running the following ace command to create a new listener class.

node ace make:listener User

# ✔  create    app/Listeners/User.ts

Open the newly created file and replace its content with the following code snippet.

app/Listeners/User.ts
import { EventsList } from '@ioc:Adonis/Core/Event'

export default class UserListener {
  public async handleRegistration (user: EventsList['new:user']) {
    console.log(user)
  }
}

Update the start/events.ts file to remove the inline event listener callback and instead reference the newly created listener class.

start/events.ts
import Event from '@ioc:Adonis/Core/Event'

Event.on('new:user', 'User.handleRegistration')

How it works?

  • The listeners classes allows you to keep your events file clean by extracting the logic for handling events inside their own classes.
  • By default, the listeners lives inside app/Listeners directory. However, you can customize inside the .adonisrc.json file.
  • AdonisJS will create a new instance of the listener class, everytime the event is emitted.

Node.js vs AdonisJS Event Emitter

The event emitter of Node.js is synchronous by nature. It means every call to emitter.emit blocks the event loop and leads to non-performant codebase. On the other hand, AdonisJS uses emittery, which is a light weight asynchronous event emitter.

Only allows one argument during emit

Emittery doesn't allow multiple arguments during the emit call and suggests to use destructuring instead. Having limitation for a single argument is not really a big drawback in comparison to the performance it brings into your applications.

API surface

We suggest you to check the API docs and do not assume identical API as the Node.js event emitter.