DEV Community

Cover image for Writing a Database backed Micro-service with Raku and Humming-Bird
Rawley Fowler
Rawley Fowler

Posted on • Updated on

Writing a Database backed Micro-service with Raku and Humming-Bird

Earlier this year I built a web-application framework for Raku called 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. Then we'll want to install Zef, if you're new to Raku, 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:

zef install Humming-Bird
Enter fullscreen mode Exit fullscreen mode

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!

use Humming-Bird::Core;

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

listen(8080);
Enter fullscreen mode Exit fullscreen mode

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

raku app.raku
Enter fullscreen mode Exit fullscreen mode

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

Hello World

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

use Humming-Bird::Core;
Enter fullscreen mode Exit fullscreen mode

Imports all of the exported members of Humming-Bird. This includes the functionality to allow creating routes, and to start the server.

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

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.

listen(8080);
Enter fullscreen mode Exit fullscreen mode

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 module maintained by the Raku Community.

zef install DBIish
Enter fullscreen mode Exit fullscreen mode

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:

sudo apt-get install sqlite3 libsqlite3-dev
Enter fullscreen mode Exit fullscreen mode

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.

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
Enter fullscreen mode Exit fullscreen mode

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

use DBIish;

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

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
)
Enter fullscreen mode Exit fullscreen mode

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:

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);
Enter fullscreen mode Exit fullscreen mode

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:

use JSON::Fast;
Enter fullscreen mode Exit fullscreen mode

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

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);
});
Enter fullscreen mode Exit fullscreen mode

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.

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 :(');
    }
});
Enter fullscreen mode Exit fullscreen mode

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:

curl -X POST -H "Content-Type: application/json" -d '{ "name": "bob", "age": 32, "email": "bob@gmail.com" }' http://localhost:8080/users
Enter fullscreen mode Exit fullscreen mode

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

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

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

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.

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]));
});
Enter fullscreen mode Exit fullscreen mode

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.

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);
    }
});
Enter fullscreen mode Exit fullscreen mode

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,
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();
}
Enter fullscreen mode Exit fullscreen mode

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.

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

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
Enter fullscreen mode Exit fullscreen mode

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!

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 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 on github.

Thanks for reading, Raku rocks!

Top comments (2)

Collapse
 
patrickbkr profile image
Patrick Böker

Minor nit: When using rakubrew download to install a Rakudo, zef is already part of the installation. No need for rakubrew build-zef in that case.

Collapse
 
rawleyfowler profile image
Rawley Fowler

Thanks for that Patrick, I fixed it up.