Node Applications
Gren has a couple different ways you can write applications that run on nodejs. We’ll go over each one here, starting with simple programs.
First we need to init a new project. But this time, we will target the node platform. In an empty directory, run:
gren init --platform=nodeThis will create a gren.json file configured for node and a src directory to hold your source code.
Simple Program
Node.defineSimpleProgram is good for short-lived programs or scripts. We’ll go over regular programs in a later section.
Below is an example of the classic Hello World program. It’s ok if you don’t understand it yet, we will go through it piece by piece in the next sections.
module Main exposing (main)
import Initimport Node exposing (Environment)import Streamimport Task
main : Node.SimpleProgram amain = Node.defineSimpleProgram init
init : Environment -> Init.Task (Cmd a)init env = Stream.writeLineAsBytes "Hello, World!" env.stdout |> Task.onError (\_ -> Task.succeed env.stdout) |> Node.endSimpleProgramCompile
To run the program you need to compile it.
First, save the above code in src/Main.gren.
Then compile and run the resulting js file:
gren make Mainnode appYou should see:
Hello, World!In the following sections we’ll go over each part of the program to see how it works.
Module definition
module Main exposing (main)This is our module declaration and it is exposing our main function which has our program definition.
Imports
import Initimport Node exposing (Environment)import Streamimport TaskHere we’re importing other modules we will need. The docs for each of these are helpful if you want to understand more:
Main function
main : Node.SimpleProgram amain = Node.defineSimpleProgram initThe main function is where Gren expects to find your program definition.
Here we’re defining a SimpleProgram.
All it needs is an init function.
Init
init : Environment -> Init.Task (Cmd a)init env = Stream.writeLineAsBytes "Hello, World!" env.stdout |> Task.onError (\_ -> Task.succeed env.stdout) |> Node.endSimpleProgramHere is the heart of our program.
-
We start with a string being passed to
Stream.writeLineAsBytes. It will take a string and write it to a stream as bytes ending in a newline. We pass it the string we want and the stream we want to write to. In this case, it’s thestdoutstream that we get from the node environment. That’s the stream that’s used to display output to the user, or to another program. But this doesn’t write tostdoutdirectly, it gives us a task that describes the effect. -
Before we can end the program, we need to handle the case where this action fails (maybe the program is being piped to a stream that is closed or doesn’t exist). So we’re passing the task from the previous function to
Task.onError. This will give us a new task.The first parameter to
onErroris a function that takes the error value and returns the task that we want. We’re using an anonymous function for this. In our case, we’re ignoring the error and returning a task that will always succeed, usingTask.succeed. The result of this task will be the original stream, so if we hit this error case, nothing will happen.In a real program, you might want to handle the error by writing to
stderrand exiting with a non-zero status code. -
Now we can pass our task to
Node.endSimpleProgram. This will give us an init task that will run our task and end the program.
This should be enough to get you started making simple programs. Scroll through the packages and docs. Reach out if you need help. And read on to learn about creating long-lived programs.
Let’s make a web server!
For more complex or long-running programs, we’ll want a full Node.Program.
This type of program uses the full Elm architecture.
Here’s an example of using Node.Program to create an HTTP server.
As usual, we’ll go through it piece by piece in the next sections.
module Main exposing (main)
import HttpServer exposing (Request, Server)import HttpServer.Response as Response exposing (Response)import Initimport Node exposing (Environment)import Task
main : Node.Program Model Msgmain = Node.defineProgram { init = init , update = update , subscriptions = subscriptions }
type alias Model = { server : Maybe Server }
serverConfig = { host = "localhost" , port_ = 3000 }
init : Environment -> Init.Task { model : Model, command : Cmd Msg }init env = Init.await HttpServer.initialize <| \serverPermission -> Node.startProgram { model = { server = Nothing } , command = HttpServer.createServer serverPermission serverConfig |> Task.attempt GotServer }
type Msg = GotServer (Result HttpServer.ServerError Server) | GotRequest { request: Request, response: Response }
update : Msg -> Model -> { model : Model, command : Cmd Msg }update msg model = when msg is GotServer result -> when result is Ok server -> { model = { server = Just server } , command = Cmd.none }
Err _ -> { model = model , command = Task.execute (Node.exitWithCode 1) }
GotRequest { request, response } -> { model = model , command = response |> Response.setBody ("You requested: " ++ request.url.path) |> Response.send }
subscriptions : Model -> Sub Msgsubscriptions model = when model.server is Just server -> HttpServer.onRequest server (\req res -> GotRequest { request = req, response = res })
Nothing -> Sub.noneSave this to src/Main.gren and run the program with:
gren make Mainnode appYou won’t see any output, but if you visit any path on http://localhost:3000 you will see a page showing the URL you visited.
You can quit the program with CTRL-c.
Now let’s see what’s happening.
Main
main : Node.Program Model Msgmain = Node.defineProgram { init = init , update = update , subscriptions = subscriptions }This time we’re using defineProgram.
This defines programs that can trigger effects (like sending HTTP responses) and subscribe to events (like HTTP requests).
We’re defining it using a record that points to our init, update, and subscribe functions.
We’ll cover those next.
Init
First we define our Model:
type alias Model = { server : Maybe Server }Our model will hold our Server.
It’s not guaranteed that our server will start (e.g. maybe the port is being used) so we have to hold it in a Maybe.
Maybe is core custom type with two variants: Just something where something is the value you want, or Nothing.
Next, we define a constant to hold our server configuration:
serverConfig = { host = "localhost" , port_ = 3000 }This will make it easier if we want to change the config, or print it when the server starts.
Next is our init function that starts the program:
init : Environment -> Init.Task { model : Model, command : Cmd Msg }init env = Init.await HttpServer.initialize <| \serverPermission -> Node.startProgram { model = { server = Nothing } , command = HttpServer.createServer serverPermission serverConfig |> Task.attempt GotServer }This is different than the init functions we’ve written so far.
It’s wrapped in a call to Init.await.
Node applications have a concept of subsystems and permissions. Subsystems are things in the world outside of your program (e.g. the filesystem, network requests, the terminal itself). Functions that interact with them will require a permission value. You can only get that value via an init task that initializes the subsystem when you start your program. This means it’s impossible for library code to trigger effects without you explicitly giving it permission to do so by passing in the permission value. You can read more about initializing subsystems in the Init docs.
In our case, we need to initialize the HTTP server subsystem with HttpServer.initialize.
This gives us permission to create the server with HttpServer.createServer.
Because starting a server is something that could fail, createServer gives us a Task that could result in a Server value, or an error.
We use Task.attempt to turn that into a command that we can send to the Gren Runtime to perform the task.
It takes a GotServer message that we will receive in our update function after the server starts.
We’ll explain that in the next section.
Update
We start by defining our message type:
type Msg = GotServer (Result HttpServer.ServerError Server) | GotRequest { request : Request, response : Response }This holds all the events that can happen in our system.
In this case there are two: GotServer and GotRequest.
We’ll receive a GotServer message after the server starts because that’s what we specified in init earlier.
It will hold a Result.
A Result is a core custom type with two variants:
one with the value we want (in our case, a Server value if the server started successfully),
and one with an error value (in our case, a ServerError if the server failed to start).
We’ll also receive a GotRequest message whenever there is an Http request because it’s what we specified in subscriptions, which we’ll cover later.
It will hold a record with a Request and a Response.
We’ll use those values to send a response back from the server, which we’ll look at soon.
Next we define our update function and match on that Msg type:
update : Msg -> Model -> { model : Model, command : Cmd Msg }update msg model = when msg isFirst we handle the GotServer message:
GotServer result -> when result is Ok server -> { model = { server = Just server } , command = Cmd.none }
Err _ -> { model = model , command = Task.execute (Node.exitWithCode 1) }Remember when we talked about Result?
If the server started successfully, we’ll get the Ok variant holding the server value, which we’re capturing in a server variable.
We return a new model that updates the server key to Just server.
Remember that the server key on our model is a Maybe, which means it must hold either Nothing or Just something, which is why we’re setting it to Just server instead of server.
We’ll see how this is used when we cover the subscriptions function later.
If the server failed to start, we’ll get the Err variant holding an error value - in our case will be a ServerError.
We know that because that’s the error type specific in the Task that we get from createServer.
We’re not using the error value so we call it _.
And we respond with a command that exits the program with a non-zero status code, which is the appropriate way to end a command line program that did not run successfully.
Next we’re handling HTTP requests in the GotRequest case:
GotRequest { request, response } -> { model = model , command = response |> Response.setBody ("You requested: " ++ request.url.path) |> Response.send }We get a GotRequest message whenever someone makes an HTTP request to the server (because we subscribed to that event - desribed later).
We’re destructuring it to get the request and response values that are attached to the message.
The request is a record with all the information about our request.
We’re using it to display the path with Response.setBody.
But in a real server you may want to use it to route different paths to different functions that handle the logic and response for that request.
The response is a value that represents our HTTP response.
It is created in the Gren runtime and required to call functions that change the response (like Response.setBody).
We turn the response into a command for the runtime to actually send the HTTP response with Response.send.
Subscriptions
subscriptions : Model -> Sub Msgsubscriptions model = when model.server is Just server -> HttpServer.onRequest server (\req res -> GotRequest { request = req, response = res })
Nothing -> Sub.noneSubscriptions are how we connect to external events.
When we return a subscription, we’re telling the Gren runtime what messages to send to our update function for the events we care about.
Earlier in the book, we’ve been returning Sub.none since we haven’t had any subscriptions.
But now, if we successfully started the server, we want to receive a GotRequest message whenever there is an HTTP request.
We do that by calling HttpServer.onRequest,
which takes the server value we saved to our model (see the GotServer branch of our update function) and a function that returns a message.
In our case, we’re using an anonymous function that returns our GotRequest message.
More examples
For more examples of node applications, see: