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.Value
s.
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 });
});
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)
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 theelm/http
package to call the respective urls. This wil let you use it like any other task. An example can be found here.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!We actually use elm-codec but the idea is the same.
Great minds think alike :)
Might also be worth taking a look at package.elm-lang.org/packages/the-...
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 useelm/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 aFile
which can go through a port.That is awesome!
Very clever use of
Value
. Do you wrap it in an opaque custom type for extra safety?No, not really. Why do you think it would be something necessary? I would love to know more about that idea.