Writing a Database backed Micro-service with Raku and Humming-Bird
Tags: raku, webdev, microservices
Published: 2023-03-05 18:13:53

Earlier this year I built a web-application framework for Raku called [Humming-Bird](https://github.com/rawleyfowler/Humming-Bird), it's come a long way since it's humble beginnings. In this post I'd like to outline how I use Humming-Bird to build fast, reliable, cloud-native services.

## Getting set up
To begin, you'll want to make sure that you have Raku installed, I recommend using [Rakubrew](https://rakubrew.org/). Then we'll want to install [Zef](https://github.com/ugexe/zef), if you're new to Raku, [Zef](https://github.com/ugexe/zef) is basically just NPM or CPAN for Raku, it's written by the fabulous Ugexe and is the default package manager people use with Raku. If you installed Raku with `rakubrew download`, you should already have it installed. If not, you can do `rakubrew build-zef` to install it for your current Raku install.

Finally, we'll want to install Humming-Bird, we can do this simply by running:
```bash
zef install Humming-Bird
```
This will pull the latest stable version of Humming-Bird.

## Hello World
Now that we've installed all of the pre-requisites let's write a simple "Hello World" web application. Humming-Bird is really simple so there isn't much to it!

```raku
use Humming-Bird::Core;

get('/', -> $request, $response {
    $response.html('<h1>Hello World!</h1>');
});

listen(8080);
```

Now if we save this script as `app.raku` then run it with:

```bash
raku app.raku
```

We can navigate to `http://localhost:8080` and we should see:

![Hello World](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1d1e2aea3a5nie1syqlh.png)

Let's break down what's going on here.

```raku
use Humming-Bird::Core;
```
Imports all of the exported members of Humming-Bird. This includes the functionality to allow creating routes, and to start the server.

```raku
get('/', -> $request, $response {
    $response.html('<h1>Hello World!</h1>');
});
```
We register a GET route on '/' and hookup a block, AKA anonymous function, AKA lambda, to handle it. This lambda must take a request, and a response, and returns a response. All of the methods provided by the response object implicitly return itself so it makes it easy to chain and return. Also, the last evaluated expression will become the return of the lambda.

```raku
listen(8080);
```
Starts a `Humming-Bird::HTTPServer` on port 8080.

## Designing our service
Now that everything is setup and working. Let's get to designing our service. Our requirements are to design a service that provides the following end-points:

- **GET** '/users': Returns all users in the database
- **POST** '/users': Creates a new user in the database if it's valid
- **GET** '/users/:id': Get's a user by their ID
- **DELETE** '/users/:id': Delete's a user by their ID

### Setting up the database
We'll also want a database for this, so we'll pull in the trusty [DBIish](https://github.com/raku-community-modules/DBIish) module maintained by the Raku Community.
```bash
zef install DBIish
```

We're going to be using SQLite3 for this so make sure you have the necessary libs installed on your machine. On an Ubuntu box that would look like:

```bash
sudo apt-get install sqlite3 libsqlite3-dev
```

Now let's hook up `DBIish` into our existing code. We'll simply always run the create script for our table whenever the server starts, just to keep it fairly simple.

```raku
use DBIish;

my $db = DBIish.connect('SQLite', :database<user-db.sqlite3>);

$db.execute(q:to/SQL/);
CREATE TABLE IF NOT EXISTS user (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name VARCHAR(20),
    age INTEGER,
    email VARCHAR
)
SQL
```

This code is fairly straight forward, but I'll break it down a little bit for people who haven't use `DBIish` before.

```raku
use DBIish;

my $db = DBIish.connect('SQLite', :database<user-db.sqlite3>);
```

First, we import `DBIish` then we assign a new variable `$db` to hold our connection, which is a SQLite3 connection, with filename `user-db.sqlite3`.

```
$db.execute(q:to/SQL/);
CREATE TABLE IF NOT EXISTS user (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name VARCHAR(20),
    age INTEGER,
    email VARCHAR
)
```

Then, we try to create our table, note this will run every time we run this script. The whole `q:to/SQL/` thing basically says: Read the following lines as a string until you read the end-sequence specified in between the /'s.

Overall our entire file looks like:
```raku
use Humming-Bird::Core;
use DBIish;

my $db = DBIish.connect('SQLite', :database<user-db.sqlite3>);

$db.execute(q:to/SQL/);
CREATE TABLE IF NOT EXISTS user (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name VARCHAR(20),
    age INTEGER,
    email VARCHAR
)
SQL

get('/', -> $request, $response {
    $response.html('<h1>Hello World!</h1>')
});

listen(8080);
```

### Setting up our end points
Now that our database is good to go, lets begin working on our endpoints, first up let's make an endpoint for getting all of the users.

We can express this simply as just returning the "jsonified" contents of our database. Humming-Bird depends on a module called `JSON::Fast`, we should import that too. Add:

```raku
use JSON::Fast;
```

to the top of our project to pull it in. It gives us access to the `to-json` and `from-json` sub-routines.

#### Getting all users
```raku
get('/users', -> $request, $response {
    my @user-rows = $db.execute('SELECT * FROM user').allrows(:array-of-hash);
    my $json = to-json(@user-rows);
    $response.json($json);
});
```

This will, select all of the users from the database as an array of hashes, then convert it to JSON with `JSON::Fast`'s `to-json`, then send it with content-type JSON via `$response.json`.

**Note**: Humming-Bird doesn't really do any magic, the `.json` and `.html` methods are just wrappers for: `.write($body, 'application/json')` and `.write($body, 'text/html')` respectively. Write just writes a `Buf` or `Str` to the body of the response.

#### Creating a user
Now let's create a user with a `POST` request. This is really simple using `$request.content`, which will convert the JSON of the request to a Raku `Hash` for you.

```raku
sub validate-user(%user) {
    # I'll leave this up to you :^)
    %user<age> > 18;
}

post('/users', -> $request, $response {
    my %user = $request.content;
    if validate-user(%user) {
        $db.execute('INSERT INTO user (name, age, email) VALUES (?, ?, ?)', %user<name>, %user<age>, %user<email>);
        $response.status(201).json(to-json(%user));
    } else {
        $response.status(400).write('Bad Request :(');
    }
});
```

I'm letting you implement your own validation, so for now we just assume the user is valid unless their age is less than 18. Then we register our `POST` route, then we get the `.content` (which is a hash converted from the JSON body), then we validate, then if it's valid send a response with status `201 Created` and write back the JSON that was sent to us. Otherwise send a bad-request response.

We can test this endpoint with:
```bash
curl -X POST -H "Content-Type: application/json" -d '{ "name": "bob", "age": 32, "email": "bob@gmail.com" }' http://localhost:8080/users
```

Now if we change the age to say 14, you should see a `400 Bad Request` response:

```bash
curl -X POST -H "Content-Type: application/json" -d '{ "name": "bob", "age": 14, "email": "bob@gmail.com" }' http://localhost:8080/users
```

And finally if we check our database by browsing to `http://localhost:8080/users` we should see the user we created. In my case I ran the insert a few times.

![users get request results](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5opaalg4evbnen6tqlpo.png)

#### Getting an individual user
This is a really simple end-point, the only difference between this and the getting all users is we have to get an ID from the request parameters.

```raku
get('/users/:id', -> $request, $response {
    my $id = $request.param('id');
    my @users = $db.execute('SELECT * FROM user WHERE id = ?', $id).allrows(:array-of-hash);

    return $response.status(404).html("User with id $id not found.") unless @users.elems == 1;

    $response.json(to-json(@users[0]));
});
```

Here we get the `id` from the requests parameters, specified in the route declaration, then we execute a DB query trying to find a user with that ID. If we don't get exactly one user back, we return a `404 Not Found` status, otherwise we return a JSON response of the user we found.

If you navigate to `http://localhost:8080/users/1` you should see a user you inserted (if not you'll need to insert some!)

#### Deleting a user
Another simple endpoint, this one will be very similar to the getting of a single user but instead we'll execute a DB delete then return nothing but a `204 No Content` response.

```raku
delete('/users/:id', -> $request, $response {
    my $id = $request.param('id');

    try {
        CATCH { default { return $response.status(404).html("User with id $id not found.") } }
        $db.execute('DELETE FROM user WHERE id = ?', $id);
        $response.status(204);
    }
});
```

Major difference here is we use a try/catch block, you don't need to. Since Humming-Bird is functional I actually think it would be better practice to use a Monad type for handling errors something like [Monad::Result](https://github.com/rawleyfowler/Monad-Result),
but that's the beautiful part of Humming-Bird, it accommodates everyone's preferences.

## Middleware
Well, your manager was so impressed that you finished this service in one-day that they decided to ask for a new feature! You should only be able to access the `DELETE` and `POST` routes if you have the magic passphrase of `foobar` in the `X-AUTH` header of your request.

Humming-Bird supports rich middleware development, using you guessed it, functions. So let's quickly write up our final middleware requirement.

```
sub auth-middleware($request, $response, &next) {
    without $request.header('X-AUTH') {
        return $response.status(401).write('unauthorized')
    }

    my $request-header = $request.header('X-AUTH');

    if $request-header ne 'foobar' {
        return $response.status(401).write('unauthorized');
    }

    &next();
}
```

A middleware is described as a function that takes a request, and a response, and finally a pointer to the next function in the [middleware chain](https://github.com/rawleyfowler/Humming-Bird/blob/6dbbeb360ca63604689768c641130c7d4009bd3e/lib/Humming-Bird/Core.rakumod#L318).

To register this middleware with a route, all we have to do is add it to the tail of the route delcaration like so:

```raku
delete('/users/:id', -> $request, $response {
    my $id = $request.param('id');

    try {
        CATCH { default { return $response.status(404).html("User with id $id not found.") } }
        $db.execute('DELETE FROM user WHERE id = ?', $id);
        $response.status(204);
    }
}, [ &auth-middleware ]); # Added auth middleware
```

Now if you hit either endpoint without an `X-AUTH`, or a non `foobar` `X-AUTH` header you will see a `401 Unauthorized` screen. Otherwise, we'll slide on through to our end-points.

## Final code
You can find the final code on [github](https://github.com/rawleyfowler/Humming-Bird-Microservice/blob/main/app.raku)!

## Conclusion
Humming-Bird makes it really easy to spin up all sorts of web-applications leveraging Raku's vibrant and welcoming ecosystem. If you're interested in deploying a service like this to production make sure you check out the [docker example](https://github.com/rawleyfowler/Humming-Bird/tree/main/examples/docker) on github. Just like any application layer framework, Humming-Bird is **NOT** designed to face the internet directly, please reverse-proxy it with NGiNX or Apache2. If you have any questions, or find an issue with Humming-Bird please refer to the [issue section](https://github.com/rawleyfowler/Humming-Bird/issues) on github.

Thanks for reading, Raku rocks!