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:
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.
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:
You should see:
In the following sections we’ll go over each part of the program to see how it works.
Module definition
This is our module declaration and it is exposing our main
function which has our program definition.
Imports
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
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
Here 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 thestdout
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 tostdout
directly, 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
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, 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
stderr
and 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.
Save this to src/Main.gren
and run the program with:
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
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:
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:
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:
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:
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:
First we handle the GotServer
message:
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:
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 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: