This article is dedicated to setting up AdonisJs websocket Client & Server API. At the end you will find a link to a monorepo with working example code (and CI/CD scripts!) and a live demo application.

It’s written for readers who are already experienced with AdonisJs (or Node.js) and React. The basics won’t be explained here, only websocket related logic.

—

Getting AdonisJs sockets to run wasn’t the easiest thing in the world, but when trying out new things is anyway? I felt the documentation was a little thin, hence the article with examples. I hope I can help those of you who struggle just the way I did.

We won’t go step by step creating a new REST API. Instead, I’m going to introduce my app idea for this demo application:

Imagine you have multiple chatrooms. Each chatroom has multiple messages in it — and the users can of course post new messages. The front-end receives the new messages via websocket communication, broadcasted by the server.

This brings us to a basic REST API structure (no authentication):

POST /rooms — creates a new chatroom

— creates a new chatroom GET /rooms/:uuid — selects and returns an existing chatroom with its messages

— selects and returns an existing chatroom with its messages POST /rooms/:uuid — creates a new message in a room, broadcasts a message to the topic based on the room’s ID (meaning only the room ID’s subscribers get the message)

You can try this out using Postman using https://adonis-sockets-api.herokuapp.com/rooms

Preparing the back-end to broadcast messages

I’ll be referring to official AdonisJs documentation. It’s expected you have a generated AdonisJs project, as we’ll be following the structure.

Installing dependencies

To install the WebSocket Provider you need to run

adonis install @adonisjs/websocket in your adonis project root.

(Copied from https://adonisjs.com/docs/4.1/websocket)

config/socket.js contains WebSocket server configuration. start/socket.js boots the WebSocket server and registers channels. start/wsKernel.js registers middleware to execute on channel subscription.

Next, register the provider inside the start/app.js file:

const providers = [

'@adonisjs/websocket/providers/WsProvider'

]

Finally, instruct Ignitor to boot the WebSocket server in the root server.js file:

const { Ignitor } = require('@adonisjs/ignitor') new Ignitor(require('@adonisjs/fold'))

.appRoot(__dirname)

.wsServer() // boot the WebSocket server

.fireHttpServer()

.catch(console.error)

—

Actual code

You can register socket channels in start/socket.js file. Mine looks like this:

'use strict'



const Ws = use('Ws')



Ws

.channel('room:*', 'RoomUpdateController')

// .middleware(['auth'])

As you can see, I’m referring to a controller instead of writing all the logic inside the socket.js file. This is optional, I prefer the Controller structure to keep my project logic separated and easy to backtrack.

What’s important to notice is the .channel('room:*') part — this is the channel you will be subscribing to. As I’m going to subscribe to different rooms (based on their ID), I need the :* dynamic parameter.

There’s also a commented line that says // .middleware(['auth']) . We won’t be doing any auth user checks, but should you want to do those, this is how you register the middleware with further instructions at https://adonisjs.com/docs/4.1/websocket-server.

—

Next, let’s generate the controller we’re referring to —

adonis make:controller RoomUpdate --type=ws

My RoomUpdateController with content inside looks like this:

The constructor() will alert us by console logging that a new subscription has been made — where socket.topic will be the room ID we’re subscribing to.

will alert us by console logging that a new subscription has been made — where will be the room ID we’re subscribing to. the onMessage() will alert us when a message is received from the client — we won’t be emitting messages from the client application, only from the server. It’s there for you, should you want to test it.

will alert us when a message is received the client — we won’t be emitting messages from the client application, only from the server. It’s there for you, should you want to test it. the onClose() will alert us whenever the client closes the connection. Use this to test whether you’re actually closing the connections.

The broadcasting itself

Our websocket logic seems to be set up — we’ve installed everything, we’re registered the channel in the start/socket.js file, we’ve linked it to a Controller and wrote some basic listening logic inside it. Now let’s go broadcast new messages!

As this article demonstrates one way websocket communication (server → client), we will be broadcasting the messages once a user POSTs a new message in a room. You can find all the API logic in the example source code linked at the end of the article. For now, I’ll be focused only to the createMessage endpoint.

app/Controllers/Http/RoomController.js

This is an extract from my RoomController.js file that contains the API logic. I’m showing only the createMessage() method as it’s the only one broadcasting.

The createMessage() returns the result (the created message object) but we’re going to ignore it in the client app and receive the new message via socket communication instead. Nothing should change if you remove this line and return a simple 200 code.

Notice the broadcast() function that’s being called — it’s exported from the socket.utils file that does most of the magic. Let’s dive into it.

—

This broadcast function is closely tailored to our needs. It expects:

an id as the first parameter, which will be our Room ID (uuid) — we need this for the dynamic topic parameter, as we only want to broadcast the message to a single room.

as the first parameter, which will be our Room ID (uuid) — we need this for the dynamic topic parameter, as we only want to broadcast the message to a single room. a type string which allows the client to separate handling logic — we’re using room:newMessage string in the Controller. This could be easily extended to various types like room:deleteMessage or room:editMessage which allow the frontend to decide what to do with the incoming data based on the type(we’ll see this later).

string which allows the client to separate handling logic — we’re using string in the Controller. This could be easily extended to various types like or which allow the frontend to decide what to do with the incoming data based on the type(we’ll see this later). a data that will end up being broadcasted, the message object in our case.

We want to access the channel for our topics, which is room:* . If there’s no channel, we can’t proceed as we can’t find our topic. We find the topic based on the received id . Again, if there’s no topic, we can’t continue (this means the client didn’t connect correctly, or at all). Once we have the topic, we broadcast the data — we can choose from 3 ways to communicate:

- emit — sends the message only to the client that produced the request

- broadcast — sends the message to everyone but the client that produced the request

- broadcastToAll — sends the message to everyone listening to said topic.

We’re using the broadcastToAll approach so there’s no difference in the client side whether you’re the one producing the message in the room or not. It simply receives a message and displays it.

Now all of the 3 ways expect two arguments — event and data .

I’m always using the string “message” for the event parameter. I find this easier as I always listen to message client side and don’t have to worry about overcomplicating things.

parameter. I find this easier as I always listen to client side and don’t have to worry about overcomplicating things. My data parameter is a joined object — made of the type property ( room:newMessage ) and the data received by the broadcast() socket util function.

The received data object by the broadcastToAll() function looks like this once it’s joined:

What the client sees upon receiving a message

—

Preparing the client to receive messages

I won’t dive into GET and POST requests to communicate with the API, or much into React syntactic sugar. In the end it shouldn’t matter what your front-end is created with, only how to receive the messages, which is pretty similar in all the frameworks.