ActionCable in Rails API
As we all know, Rails 5 introduced a built-in WebSockets handling module called ActionCable. It’s a full front and backend package setting up both the client and the server in a typical Rails-style magical way, exposing to the developer just enough to get things going without having to think too much about how it’s implemented. This kind of approach is very convenient and user-friendly and is arguably one of the reasons Rails got to be such a popular tool — however it has a way of rearing its ugly little head as soon as you want to do something differently.
In our case this different way was using a bare Rails API instead of the full-stack monolith. By browsing obscure StackOverflow discussions, looking into ActionCable’s code and a lot of experimenting we have managed to make it work — and it turned out to be real simple — so we decided to share this knowledge for all future devs who might find themselves in the same predicament. Before reading on we suggest to make yourself familiar with the ActionCable’s official docs.
To begin with, set up ActionCable in your Rails API app the regular way — by mounting the server to your preferred path in the routes.rb file, for example like this:
This will create a new route in your app to initiate a WebSocket connection to.
The next step is authenticating your users in the ApplicationCable::Connection class. Think of it as the ActionCable’s ApplicationController — a base class that configures all the connection and authorization details, including the current_user helper. The preferred way to authenticate your users is by using cookies, like this:
However with an API app you might find yourself in a situation where no authentication cookies are available. If using a JWT (JSON Web Token) authentication you can authenticate your users by sending the current user’s token to the Connection class’ connect as a request parameter, decoding the token and finding the user by the identification details submitted in its payload, like this (the following example uses the Knock gem for authentication and assumes you’re sending the user ID in the ‘sub’ section of the payload):
Now you can initiate connections to the API’s WebSockets URL. For the frontend part of your application you can use a dedicated actioncable npm package, but you can also work with any WebSockets solution of your choosing.
Having set up the connection, we now move to Channels. Think of them as controllers — classes that accept the authenticated connection and trigger actions. They use a predefined set of callbacks that handle basic WebSockets actions like subscribing and unsubscribing, but can also utilize custom methods that you define. To start using a channel first you need to define it:
In the example above we have created a new class called MyChannel. It inherits from the ApplicationCable::Channel — a base class that can be used to define behaviours and methods common to all your application’s channels. The ApplicationCable::Channel and ApplicationCable::Connection are automatically created when you set up your Rails API project.
WebSockets messages sent to your API app need to have a specific structure:
The JSON keys available are command, identifier and data (the last one we’ll cover in a moment). The command key informs your connection what action you want to perform. The identifier, as the name suggests, identifies the channel class that you want to handle the specified action, but it can also identify a specific model that you want to track by passing a model_id key next to the channel identifier, like this:
The last option we’ll cover here is calling a specific method in your channel class:
In the example above we call some_method in MyChannel passing args to it. If we looked into the some_method(args) upon execution and printed its arguments, we’d see that what is passed to the method is a following hash:
Here’s an example of a method call:
Of course you’d probably want to pass something more than just a bare string value as an argument to the method, which brings us to the most important thing: all the values of the basic message keys (command, identifier, data) MUST be JSONized strings. ActionCable parses them by default, so the de facto structure of a subscription message needs to look as follows:
The same applies to all other messages you send to your ActionCable server. The most important thing to remember here is that the above applies also to the arguments sent in the data key: as the ActionCable parses it using the ActiveSupport::JSON.decode() method, the values of the decoded JSON also need to be strings, otherwise JSON::ParserError: 743: unexpected token will be raised. So the actual message you send should look like this:
And that’s it — some careful JSON.parse-ing and you should get your Rails API WebSockets implementation up and running in no time.