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
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);
Now if we save this script as app.raku
then run it with:
raku app.raku
We can navigate to http://localhost:8080
and we should see:
Let's break down what's going on here.
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.
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.
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 module maintained by the Raku Community.
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:
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.
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.
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:
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:
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
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.
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:
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:
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.
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]));
});
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);
}
});
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();
}
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
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)
Minor nit: When using
rakubrew download
to install a Rakudo, zef is already part of the installation. No need forrakubrew build-zef
in that case.Thanks for that Patrick, I fixed it up.
Some comments may only be visible to logged-in visitors. Sign in to view all comments.