DEV Community

loading...

Elm in the server (or anywhere else) with promises

eberfreitas profile image Éber Freitas Dias ・4 min read

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:

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

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!

Discussion

pic
Editor guide
Collapse
pablohirafuji profile image
Pablo Hirafuji

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.

Collapse
pablohirafuji profile image
Pablo Hirafuji

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!

Collapse
eberfreitas profile image
Éber Freitas Dias Author

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

Collapse
pablohirafuji profile image
Pablo Hirafuji

Great minds think alike :)

Collapse
lukewestby profile image
Luke Westby

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

Collapse
eberfreitas profile image
Éber Freitas Dias Author

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