How does elm-pages work?
Is elm-pages
magic? Does it use native code or hacks to give me side effects or things like BackendTask
? Does my mental model of Elm and pure functions change?
The answer to all of these questions is no!
It might feel a bit like magic to be able to run full-stack Elm. And while it may be a bit of a mental model of how all these pieces fit together, it's all just Elm, plus some code generation, and generating two Elm applications. One backend version that runs at build-time, or on each server-request if it's a server-rendered route. And one frontend version that takes the initial HTML and data
provided by the backend app. So it gives you a seamless experience of writing in one context, but this is more a convention and an abstraction than magic. elm-pages
does also use the Lamdera compiler to get some automatic Bytes
Encoders and Decoders that it uses to communicate between the frontend and backend apps. This doesn't change your guarantees about using Elm, though it is a key piece that makes it seamless to build your elm-pages
routes. This is how the data
is resolved from the backend app, but is then accessible to the frontend app. Hopefully you'll find it very predictable once you understand this general architecture and set of abstractions.
elm-pages
does not change Elm's pure functions. In fact, it heavily relies on this as a basis for its design! It does give you BackendTask
which allows you to execute data. It may not seem like it, but this is nothing more than a plain old Elm Custom Type! That Custom Type is handled with some special behavior by the framework to give you a lot of functionality, but it doesn't in any way break the guarantees you know and love from any other Elm application. Think of BackendTask
like a Cmd
or Effect
. It's a special type which you pass up to the runtime to ask it to do something for you. Since Elm code is pure, it can't perform side-effects. BackendTask
s will not do anything unless they're passed to the elm-pages
framework. Since the framework has its own Model
, update
, etc., it's able to go perform side-effects to resolve those BackendTask
s for you. You can think of it like a wrapper around The Elm Architecture.
The secret to elm-pages is that it creates an Elm app that calls the code you define (in your Route Modules, etc.), wires up routing for you, manages some state in its Model (it's just an Elm app, so it can have its own Model
and share some of the things it keeps in there with you - in fact, that's exactly what RouteBuilder.App
is, some state that is manged for you by the framework!).
So the main techniques elm-pages uses to give what feels like magic are:
elm-pages
takes the code you write in your Route Modules and it wires it up to give you routing, etc. with some generated code (also gives you aRoute.elm
module, for example)elm-pages
creates two Elm applications from your Route Modules. One that runs in the browser, one that runs on the server!data
andaction
are never run in the Browser, they are only run from your backend (which could be your build step if it's a pre-rendered route)- The app that
elm-pages
makes to run on your server has aport
that it uses. Since this runs in the backend, this runs in a NodeJS context! So that means this port is able to do things like read files, or even run arbitrary NodeJS functions! This is whatBackendTask.Custom.run "myNodeJsFunction"
does,elm-pages
just tries runningmyNodeJsFunction
from yourcustom-backend-task.ts
file. It transpiles that file with esbuild, which is why you can write it in TypeScript. elm-pages
has a few places that accept aBackendTask
(like your Route Modules'data
andaction
functions). It takes aBackendTask
and is able to use theport
in its generated code for the Server app to step through and resolve any data that's needed until theBackendTask
is complete. Then it passes that resolved data through to the user code.
That's all it is really! It might feel magical, but it's really just a couple of abstractions that are used over and over again. It's the difference between you (the user) running a port
, vs. the framework (elm-pages
) using a port internally. But under the hood, elm-pages
is just using init
, update
, Model
, etc. BackendTask
's are resolved in the Server app that elm-pages
generates just using these vanilla Elm things, so it's all just an abstraction that's built on regular Elm!