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
- Create a Rails channel.
- Hook into model lifecycle event.
- Consume event’s in your new realtime JSON API.
Cheers @BijanRahnema