Skip to content

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:

Terminal window
gren init --platform=node

This 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.

src/Main.gren
module Main exposing (main)
import Init
import Node exposing (Environment)
import Stream
import Task
main : Node.SimpleProgram a
main =
Node.defineSimpleProgram init
init : Environment -> Init.Task (Cmd a)
init env =
Stream.writeLineAsBytes "Hello, World!" env.stdout
|> Task.onError (\_ -> Task.succeed env.stdout)
|> Node.endSimpleProgram

Compile

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:

Terminal window
gren make src/Main.gren
node app

You should see:

Hello, World!

In the following sections we’ll go over each part of the program to see how it works.

Module definition

src/Main.gren
module Main exposing (main)

This is our module declaration and it is exposing our main function which has our program definition.

Imports

src/Main.gren
import Init
import Node exposing (Environment)
import Stream
import Task

Here we’re importing other modules we will need. The docs for each of these are helpful if you want to understand more:

Main function

src/Main.gren
main : Node.SimpleProgram a
main =
Node.defineSimpleProgram init

The 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

src/Main.gren
init : Environment -> Init.Task (Cmd a)
init env =
Stream.writeLineAsBytes "Hello, World!" env.stdout
|> Task.onError (\_ -> Task.succeed env.stdout)
|> Node.endSimpleProgram

Here is the heart of our program.

  1. 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 the stdout stream 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 to stdout directly, it gives us a task that describes the effect.

  2. 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 onError is 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, using Task.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 stderr and exiting with a non-zero status code.

  3. 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.

src/Main.gren
module Main exposing (main)
import HttpServer exposing (Request, Server)
import HttpServer.Response as Response exposing (Response)
import Init
import Node exposing (Environment)
import Task
main : Node.Program Model Msg
main =
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 Msg
subscriptions model =
when model.server is
Just server ->
HttpServer.onRequest server
(\req res -> GotRequest { request = req, response = res })
Nothing ->
Sub.none

Save this to src/Main.gren and run the program with:

Terminal window
gren make src/Main.gren
node app

You 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

src/Main.gren
main : Node.Program Model Msg
main =
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:

src/Main.gren
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:

src/Main.gren
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:

src/Main.gren
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:

src/Main.gren
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:

src/Main.gren
update : Msg -> Model -> { model : Model, command : Cmd Msg }
update msg model =
when msg is

First we handle the GotServer message:

src/Main.gren
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:

src/Main.gren
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

src/Main.gren
subscriptions : Model -> Sub Msg
subscriptions model =
when model.server is
Just server ->
HttpServer.onRequest server
(\req res -> GotRequest { request = req, response = res })
Nothing ->
Sub.none

Subscriptions 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: