DEV Community

Éber Freitas Dias
Éber Freitas Dias

Posted on


Elm in the server (or anywhere else) with promises

Elm was created to run on the browser, but every once in a while someone will ask about how to run Elm in a server environment.

At my current job, we needed to sync several clients and persist that shared state somewhere, so we thought it would be a good idea if the server could act like another client that could persist that state in a centralized place.

For that, we created a Node/Express server to run our Elm code and at first, it was very hackish.

In a server-like environment, you mostly always have a request and a response tied together. You ask for something and you might get what you requested or an error. It doesn't matter, for every request, there is a response.

But Elm doesn't work like that if you wanna talk to the outside world. Yes, you can use ports for outside communication, but ports follow the actor model of message passing. So contrary to the request/response nature of server communication, you can only send and receive messages in Elm. That might sound like the same thing but it is not. You can receive a message without ever sending one in the first place. Or send a message without the need to wait for a message back. You can send a message and receive multiple messages back and so on. There is no coupling between sending and receiving a message and that makes Elm kinda unsuitable for a server software where request/response messages are tied.

After looking for better solutions I came across this post in the forums where the user joakin made a clever suggestion: just send the response object from the JavaScript side to a port and send it back through another port when replying to whatever it was requesting. Use the response object to send a proper response to the right client and there you go. You can see an example of that on this helpful repository.

That is something I didn't know: you can pass any JavaScript value as a Json.Decode.Value to Elm, even functions. Of course, you can't do much with them inside Elm but in this case, it helps to tie a specific function call to the message we will send back.

The idea is great and helps us to have some type of tied request/response flow. The problem is when we needed to test the integration. It was easier to bypass all the server stuff and focus on the interoperation between Node and Elm directly. Or even worse, what if the software we were writing wasn't a Node/Express server at all? That is when my boss and co-worker Nate suggested we used promises. Instead of sending the response object from Express to Elm, we could send the resolve function from a promise!

I have made a fork from the example code above with these changes. You can check it out here.

On the Elm side, nothing much has changed. I just made a few naming changes to better reflect the new nature of the interoperation with the JavaScript code. But other than that, we didn't have to change much to make this approach work as both the previous response object that was being sent from Express and the new resolve function from the promise are both just Json.Decode.Values.

The real magic is on the JavaScript code. It is a little bit more complex but it decouples the Elm code and ports from Express itself, making it possible to use that approach virtually anywhere. Here is the bit that makes everything work:

  .createServer((request, res) => {
    new Promise(resolve => app.ports.onRequest.send({ request, resolve }))
      .then(({ status, response }) => {
        res.statusCode = status;

app.ports.resolve.subscribe(([{ resolve }, status, response]) => {
  resolve({ status, response });
Enter fullscreen mode Exit fullscreen mode

So, it is possible to use Elm in the server, and I would argue that with that approach if you need some kind of tied request/response integration, you can use Elm anywhere where you can use Node. But is it useful? In our case, where we wanted to use most of the code from our client on the server it was a total win, but I would think twice if I wanted to build a full server with Elm as it just doesn't have all the things you will need to make it a good developing experience, although it would be possible.

Maybe Roc will be the language we will use for cases like that. Can't wait for it!

So, what do you think about this approach? Have you done something similar or vastly different to solve the same problem?

Thanks for reading!

Top comments (8)

pablohirafuji profile image
Pablo Hirafuji • Edited

Well-thought! I have been using Elm on AWS lambda, and has been great to share the types and validations. My approach is the same as yours, but I pass the callback function that lambda provides and call it when it returns to javascript, without promise.

An approach that can help with the heavy use of NodeJs/Javascript function calls is to tweak the XMLHttpRequest object (on NodeJs you have to install a polyfill, like xhr2) to intercept certain urls and use the elm/http package to call the respective urls. This wil let you use it like any other task. An example can be found here.

pablohirafuji profile image
Pablo Hirafuji • Edited

Another neat possibility when programming Elm in the frontend and backend is to use the great elm-serialize package. You only have to write the codec and it will encode and decode from/to json/string/bytes. It already comes with a pretty neat versioning strategy!

eberfreitas profile image
Éber Freitas Dias

We actually use elm-codec but the idea is the same.

pablohirafuji profile image
Pablo Hirafuji

Great minds think alike :)

rupertlssmith profile image
Rupert Smith • Edited

Might also be worth taking a look at

This follows the same model described above - the request (and response) comes into a subscription port, and the filled in response goes back out a port. The elm-serverless framework ties those 2 things together by maintaining a connection id and letting you have a Model as context over the whole request/response cycle.

elm-serverless has a modified XMLHttpRequest object, that lets you use elm/http just as you would in a browser application. Your HTTP requests will trigger events in the application with the correct context attached.

It can run in standalone mode too, not just for deploying to the cloud, although the standalone mode is more meant for development.

One issue is requests with binary data - since Elm ports cannot pass Bytes. There is probably a workaround for this by putting the data in a File which can go through a port.

eberfreitas profile image
Éber Freitas Dias

That is awesome!

lukewestby profile image
Luke Westby

Very clever use of Value. Do you wrap it in an opaque custom type for extra safety?

eberfreitas profile image
Éber Freitas Dias • Edited

No, not really. Why do you think it would be something necessary? I would love to know more about that idea.

Visualizing Promises and Async/Await 🤯

async await

Learn the ins and outs of Promises and Async/Await!