If you're not familiar with ExpressJS, it's just a lightweight framework that makes building web applications with NodeJS really easy:
const express = require('express')
const app = express()
app.get('/', (req, res) =>
res.send('Hello from homepage!')
)
app.listen(3000, () =>
console.info('Ready at http://localhost:3000')
)
This code example creates an Express app
, registers a route for /
using
app.get
, and then starts the web server on port 3000
.
Since Elm compiles to JS, I was curious what it would be like to use Elm to define
my routes by using Platform.worker
and ports.
Let's build it from scratch together!
To understand how Platform.worker
actually works, lets make a simple sample app
$ mkdir elm-express
$ cd elm-express
$ npm install -g elm
$ elm init
These commands will create a new folder called elm-express
, enter that folder,
install the elm
command line stuff, and then elm init
will setup your project!
Your project should now look like this:
|- elm-express/
|- src/
|- elm.json
Let's create src/Main.elm
and start coding!
First we define our module:
module Main exposing (main)
The module is called Main
, and it will expose a function called
main
. That function will be the entrypoint to our Elm app.
Let's define main
next:
module Main exposing (main)
main =
Platform.worker
{ init = init -- TODO
, update = update -- TODO
, subscriptions = subscriptions -- TODO
}
Our main
function is going to define a Platform.worker
program. This
program needs three functions to work:
init
- Called on startup to initialize the app.update
- Called everytime we get a message. It returns the updated model.subscriptions
- We can also register things to send us messages from JS.Before defining each of these, we should define the types for the application so things make a bit more sense later on:
module Main exposing (main)
type alias Flags = ()
type alias Model = String
type Msg = NoOp
main : Program Flags Model Msg
main =
Platform.worker
{ init = init -- TODO
, update = update -- TODO
, subscriptions = subscriptions -- TODO
}
Above, we're saying that we are going to receive no flags on startup. We use the
empty parentheses to let Elm know that it shouldn't expect any Flags
.
Next, we are saying that our Model
is just going to be a String
. We'll update
that one later when we have something interesting to keep track of.
Finally, we defining all the possible messages our app can receive with Msg
. For
now, we are just going to define one message called NoOp
that has no effect.
(This will change soon too!)
init : Flags -> (Model, Cmd Msg)
init flags =
( "", Cmd.none )
The init
function expects our flags to come in as input, and will return a pair
of two things when it's done:
The initial model (just an empty string)
Any side effects we want to create on startup (Cmd.none
means no side effects)
The next function we should define is update
, the thing that returns an update
Model
when a new Msg
comes in.
update
also receives the current model as the second input (model
) in case it
needs to use the current state to determine the new state:
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
NoOp ->
( model, Cmd.none )
Here we use a case of
expression to match on all the possible Msg
types we
might get. For now, we've only defined one type of Msg
called NoOp
.
When we receive a NoOp
message, we simply return the model
as it is, and use
Cmd.none
to say we want no side effects also.
If we add more types of Msg
later on, this case of
will allow the Elm compiler
to bring us back to update
so we don't forget how to handle the new Msg
!
The subscriptions
function takes in the current Model
and returns subscriptions
that should be able to send Msg
later. For now, we don't need any subscriptions
at all.
We'll just return Sub.none
, which says "we don't want any subscriptions, leave
me alone".
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.none
Altogether, our src/Main.elm
file should look like this:
module Main exposing (main)
type alias Flags =
()
type alias Model =
String
type Msg = NoOp
main : Program Flags Model Msg
main =
Platform.worker
{ init = init
, update = update
, subscriptions = subscriptions
}
init : Flags -> (Model, Cmd Msg)
init flags =
( "", Cmd.none )
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
NoOp ->
( model, Cmd.none )
subscriptions model =
Sub.none
Let's use elm make
in our terminal to compile this to JS:
$ elm make src/Main.elm --optimize --output server/elm.js
We should see Success! Compiled 1 module.
, which means we are ready to use
elm.js
in our NodeJS application!
To create a new ExpressJS app, we'll need to get a package.json
using npm init
$ npm init -y
We use the -y
option here so it doesn't ask us 10 questions, and just uses the
default stuff.
Now your folder should look something like this:
|- elm-express/
|- elm-stuff/
|- server/
|- elm.js
|- elm.json
|- package.json
|- src/
|- Main.elm
with a package.json
file that looks like this:
{
"name": "elm-express",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo 'Error: no test specified' && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
Let's update main
from index.js
to server/index.js
. Then we should create
the server/index.js
file and give it the content from the beginning of this post:
const express = require('express')
const app = express()
app.get('/', (req, res) =>
res.send('Hello from homepage!')
)
app.listen(3000, () =>
console.info('Ready at http://localhost:3000')
)
We can't run this program until we install the express module from npm:
$ npm install --save express
While we are at it, let's install the elm
package as a dev dependency, so people
who clone our repo don't have to globally install elm
(like we did with -g
at
the beginning of this post):
$ npm install --save-dev elm
In the scripts section, lets define two npm scripts to build and start our server.
Our package.json
should look like this now:
{
"name": "elm-express",
"version": "1.0.0",
"description": "",
"main": "server/index.js",
"scripts": {
"start": "node server/index.js",
"build": "elm make src/Main.elm --optimize --output server/elm.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"express": "^4.16.4"
},
"devDependencies": {
"elm": "^0.19.0-bugfix2"
}
}
Now running we can run npm run build
to recompile server/elm.js
and npm start
to get our server up and running!
Let's see if our express server works:
$ npm start
Your console should say:
Ready at http://localhost:3000
And visiting that URL in your browser will display "Hello from homepage!"
Since server/elm.js
is just a JS file, we can require
it in our application
like this:
const express = require('express')
const { Elm } = require('./elm.js')
console.log('Elm', Elm)
const app = express()
// ... the rest of the code
You'll see our program outputs this now:
Elm { Main: { init: [Function] } }
Ready at http://localhost:3000
The Elm
variable has a property Main
, which has a function called init
.
Calling that function will start up the Elm app!
const express = require('express')
const { Elm } = require('./elm.js')
const elmApp = Elm.Main.init()
console.log('elmApp', elmApp)
const app = express()
// ... the rest of the code
Hooray! We got this as the output:
elmApp {}
Ready at http://localhost:3000
elmApp is an object containing all the ways NodeJS can interact with our Elm app.
And right now it's completely empty, because we didn't make any ports yet.
In Elm, we don't share code with Javascript. The way we communicate with JS is with a simple concept called "ports".
A port is basically a one-way channel of sending messages either:
Let's update our Main
Elm module to become a port module that sends a message
to JS when our Elm app is ready!
port module Main exposing (main)
-- The rest of our code
First we'll need update the first line of our code so the Main module is allowed to use ports.
The next thing we'll do is define an outgoing port that will be able to send messages
(of type String
) to JS as a side effect (thats what Cmd
is for):
port module Main exposing (main)
port outgoing : String -> Cmd msg
-- The rest of our code
Finally, let's send JS a message on startup, by updating init
:
init : Flags -> (Model, Cmd Msg)
init flags =
( "", outgoing "Elm is ready!" )
If we rebuild our Elm app now with that dope build
script:
$ npm run build
And rerun our app with npm start
, we'll see this:
elmApp { ports:
{ outgoing:
{ subscribe: [Function: subscribe],
unsubscribe: [Function: unsubscribe] } } }
Ready at http://localhost:3000
Woohoo! Let's update server/index.js
to subscribe to the outgoing port:
const express = require('express')
const { Elm } = require('./elm.js')
const elmApp = Elm.Main.init()
elmApp.ports.outgoing.subscribe(function (message) {
console.log(message)
})
const app = express()
// ... the rest of the code
Running npm start
one more time:
Ready at http://localhost:3000
Elm is ready!
Our Elm app is sending messages to NodeJS. We're using Elm on the backend!
Next, we'll make Elm provide ExpressJS routes.
But that was a lot of stuff I threw at you. Take a break, grab some coffee, I'll see you soon in part two!