A beginner’s guide to websockets in Elm and Crystal

This post is aimed at people who have heard about websockets but never have had the opportunity to use them. Prior to writing this post I was just that: someone who had heard about websockets but had never had an opportunity to use them.

An introduction to websockets

If you don’t know what websockets are, visualise them like a pipe running from the server to the client. Messages can be sent through that pipe in either direction. What do we mean by messages? The messages are simply strings.

The server sends the message “Hello World!” to the client

You might be wondering: how is this different from http requests? Ah, I’m glad you asked! Http requests would be like building a pipe that disintegrates once you send a message. Therefore, you need to build a new pipe each time either the server or client wants to communicate with each other. Sounds inefficient doesn’t it? That’s why websockets are so appealing since the pipe persists until the client or server decides to close it. Another drawback with http is that the server can’t push data to the client until the client asks for data.

Building a simple chat app

In this blog post, we’re going to build a super simple chat app. You can see a live demo here. To get the most out of the demo, open the link in two browser windows and have a chat conversation with yourself. If you’re reading this on mobile, the gif below will give you a sense of what the app is like.

Max and Linda try out the Crystal Elm chat

Crystal Backend

We’re going to be using Crystal for the backend and Elm for the frontend. If you’ve never heard of Crystal, it’s a young language which has been described as being “Fast as C, Slick as Ruby”. We’re using Crystal, more specifically the Kemal framework, for three reasons:

  1. Kemal has superb performance for web sockets.
  2. The Crystal language has beautiful Ruby-like syntax.
  3. Deploying a Kemal app on Heroku is a piece of cake.
Crystal: Fast as C, Slick as Ruby

I’m not going to spend too much time describing the steps for creating the backend because this excellent blog post by Kemal’s author, Serdar Doğruyol, covers everything you need to know. Referring to the the above blog post:

  1. Install the latest version of Crystal. At the time of writing, the latest version is 0.19.4.
  2. Create a new project called kemal-chat-elm using the command crystal init app kemal-chat-elm in your terminal.
  3. Add and install the shard for Kemal to your project. Shards are like Ruby Gems or NPM modules.

For this app, the code which we place in src/kemal-chat-elm/kemal-chat-elm.cr is shown below. This is straight out of the linked blog post. Make sure to add Kemal.run at the end of your file.

require “kemal”
SOCKETS = [] of HTTP::WebSocket
ws “/chat” do |socket|
# Add the client to SOCKETS list
SOCKETS << socket
  # Broadcast each message to all clients
socket.on_message do |message|
SOCKETS.each { |socket| socket.send message}
end
  # Remove clients from the list when it’s closed
socket.on_close do
SOCKETS.delete socket
end
end
Kemal.run

Run crystal src/kemal-chat-elm.cr in your terminal to start your Kemal server. If all goes well, you should have a server running on 0.0.0.0:3000 which is equivalent tolocalhost:3000.

Elm Frontend

For the frontend, we’re going to use Elm because:

  1. We’re going to get zero runtime exceptions.
  2. It’s blazing fast.
  3. I find that we can write maintainable code without even trying.

This project uses Elm 0.17.1. I’m going to assume that you’ve worked through the Elm guide up to the section on websockets as this post is not intended to be an introduction to Elm.

Create a new Elm project. I called mine elm-chat. I recently discovered a handy tool called create-elm-app that handles of a lot of the boilerplate involved in creating a new Elm project and I recommend that you try it out.

You can create this project inside the above kemal-chat-elm directory or in its own directory. Personally, I first started building it outside and then later moved it into kemal-chat-elm so that I wouldn’t have to manage two git repos. Just make sure to add the elm-stuff directory to the .gitignore file.

Next, open up your elm-package.json and ensure that you have the following dependencies.

"dependencies": {
"elm-lang/core": "4.0.1 <= v < 5.0.0",
"elm-lang/html": "1.0.0 <= v < 2.0.0",
"elm-lang/websocket": "1.0.0 <= v < 2.0.0"
},

Below is the starting Elm code for our chat app. It’s not the final code that you see in the live demo but it gets us 90% there. The easiest way to run this program and see it in your browser is by entering elm-reactor src/Main.elm in the terminal.

Receiving a websocket message

Elm makes it easy to handle websockets. At the beginning of this post, we talked about sending and receiving messages through a pipe. Let’s take a look at Line 85 where we handle receiving a message.

WebSocket.listen "ws://0.0.0.0:3000/chat" NewChatMessage

We’ve got this bit of code sitting in the subscription section. Elm can subscribe to a bunch of things from the outside world such as time, window size and websockets. That server you see there, ws://0.0.0.0:3000/chat, is the websocket server running in Kemal. Whenever we receive a new message from the above address, we handle it with the NewChatMessage update message.

The NewChatMessage defined on line 53 simply takes the new chat message and appends it to the existing list of chat messages. At this point, the chat messages are anonymous because we haven’t asked the user for their username.

Sending a websocket message

When the user enters their chat message and clicks on submit, we would like to send the message to our Kemal server so that it can send that message to all the clients participating in this chat. In addition, we want to clear the text input where it says “Hello” so that the user can enter their next message.

Let’s take a closer look at the code that does this on lines 44 to 48.

PostChatMessage ->
let
message = model.userMessage
in
{ model | userMessage = "" } ! [ WebSocket.send "ws://0.0.0.0:3000/chat" message ]

In the let block, we store the chat message in the variable message. Next, we clear the userMessage in the model by replacing it with an empty string — that’s why we needed to store this value in message in the let block so that we wouldn’t lose it. Lastly, we send the user’s chat message to the Kemal server using WebSocket.send "ws://0.0.0.0:3000/chat" message.

That WebSocket.send "ws://0.0.0.0:3000/chat" message is a command that we’re sending to Elm. Our user’s chat message is leaving the comforts of our Elm program and going into the scary outside world. But don’t worry, if anything bad happens, such as losing the connection to the websocket server, Elm will handle it for us.

The world outside of our Elm program can be a scary place

If we can’t reach our Kemal server, Elm’s websocket module will keep trying to connect to the Kemal server with an exponential backoff strategy. You can see this for yourself if you shutdown the Kemal server, post a message from Elm, and then start the Kemal server again. The chat message you sent using Elm will not be lost.

Websocket server, where art thou?

In the Elm app above, we’re hardcoding the websocket server address as ws://0.0.0.0:3000/chat which is fine if we’re only going to run this on our local machine. But what happens when we deploy our app? Our websocket server will not be located at 0.0.0.0:3000. We could obtain the production websocket server address and paste it into our Elm app but that’s rather brittle, don’t you think?

Furthermore, if our Elm app is served over https, Chrome will not let us use a regular websocket connection and give us an error insisting that we use a secure websocket connection (wss). How will our app handle this?

Solution

So far, we have been running our program using Elm Reactor but we can compile the code from Main.elm into a javascript file and import it into an html file. The benefit of doing this is that we can pass arguments to the Elm app on initialisation. In our case, we can pass the websocket endpoint. Another benefit is that we can use external stylesheets — more on this later.

We create an index.html and elm.js and put it in a public folder in our Kemal app. The code in index.html is something we write and is shown below. The code for elm.js is generated using the following command from the terminal: elm make src/Main.elm --output elm.js

We add a bit of javascript in the body of our index.html that figures out the address of the websocket server as shown on lines 10 and 11. That information is passed into Elm when it is initialised on line 14. To access this information within our Elm app, we use Html.programWithFlag as opposed to Html.program.

main : Program Flags
main =
Html.programWithFlags
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
}
type alias Flags =
{ websocketHost : String }

Notice that we need to create a type alias called Flags so that Elm knows what the Flag structure is going to be. To access the flag information, we update the model and init function as follows.

type alias Model =
{ chatMessages : List String
, userMessage : String
, websocketHost: String
}
init : Flags -> (Model, Cmd Msg)
init flags =
( Model [] "" flags.websocketHost
, Cmd.none
)

Final Touches

The final version of the code that you have seen in the live demo does some extra things. Namely:

  • It prompts the user for a username and it appends that to the chat message.
  • It adds styling using skeleton.css — if you’re doing any style work with Elm that requires external stylesheets, I recommend that you use something like elm-live in development as opposed to the elm-reactor because elm-reactor does not handle external stylesheets. Another benefit is that elm-live has live reloading which elm-reactor does not have.

If you would like to deploy this Kemal app to Heroku, follow the instructions on heroku-buildpack-crystal.

Happy chatting!