05 Oct 2022

Rails 7 Live JSON API

In this tutorial I want to show you how it’s possible to create a Ruby on Rails To-Do backend that is serving JSON via API and communicates changes in realtime to all connected clients via web sockets.

Let’s create a simple todo API. I will on purpose neglect all the other aspects like (authentication/authorization/validation) to keep this as pure as possible.

Setup

First of all let us create our sample application:

rails new todos-api --api

Now let us create our todo model:

rails g scaffold todo name completed:boolean
rails db:migrate

I added two todos to our seeds.rb:

Todo.create(name: "First Task", completed: false )
Todo.create(name: "Second Task", completed: false )

Now seed the database and start the server:

rails db:seed
rails s

Congratulations we now have a simple todos API backend up and running that returns an array of our two seeded todos:

curl http://localhost:3000/todos

Setting up the channel

Now we want to communicate changes to our todos as soon as they happen to connected JavaScript clients written in React, Ember, Angular, Svelte, Vue or any other fancy frontend-framework.

First we need to mount our websocket endpoint in our rails app. Therefore we modify our development.rb file and add these two lines. We don’t cover testing and prod here but you get the idea.

config.action_cable.disable_request_forgery_protection = true
config.action_cable.mount_path = '/websocket'

Well done. We expose now this websocket endpoint:

ws://localhost:3000/websocket

Now let’s create a channel we use to publish changes to our todos:

rails g channel Todos

We keep this channel very simple:

class TodosChannel < ApplicationCable::Channel
  def subscribed
    stream_from "todos"
  end

  def unsubscribed
    puts "We lost a client."
  end
end

The subscribed method is invoked whenever a client connects. It sets the key the connected client is notified on. Here we simply chose “todos”.

You know what? We can now transmit data over the websocket to our connected clients:

ActionCable.server.broadcast("todos", {data: "hello world"})

Watching the Todo model and broadcasting changes

There are many ways we could go here but I promised to keep it simple. ActiveRecord Models in Rails have callbacks for CRUD and we make use of these callbacks now to transmit the changes. We want to broadcast to every established websocket connection.

class Todo < ApplicationRecord
  validates :name, presence: true

  # Hook into the create lifecycle
  after_create :broadcast_create

  private

  # We broadcast that a new todo was created.
  def broadcast_create
    ActionCable.server.broadcast('todos', {
      data: {
        event: "created",
        todo: self.as_json
      }
    })
  end
end

Now our live server is done. Time for a minimal frontend to consume our API.

Connect a frontend with plain websockets

I just created a plain html file with javascript that outputs the received data:

<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport"
        content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <meta name="author" content="Bijan Rahnema">
  <title>Screen Island Rails Todos API Demo-Frontend</title>
</head>
<body>
<script>
  const onOpen = (ws, store, code) => evt => {
    console.log("WS OPEN");
    const msg = {
      command: 'subscribe',
      identifier: JSON.stringify({
        channel: 'TodosChannel',
      }),
    };
    socket.send(JSON.stringify(msg));
    console.log(msg);
  }

  const onClose = (ws, store) => evt => {
    console.log("WS CLOSE");
  }

  const onMessage = (ws, store) => evt => {
    const data = JSON.parse(evt.data);
    if (data.type === 'ping') {
      return;
    }
    console.log("WS MESSAGE");
    console.log(data);
  }

  const url = 'ws://localhost:3000/websocket'
  socket = new WebSocket(url);
  socket.onmessage = onMessage(socket);
  socket.onclose = onClose(socket);
  socket.onopen = onOpen(socket);
</script>
</body>
</html>

Open your dev console and you can inspect incoming messages etc.

That’s it for this tutorial.

Wrap-Up

  1. Create a Rails channel.
  2. Hook into model lifecycle event.
  3. Consume event’s in your new realtime JSON API.

Cheers @BijanRahnema