In this spirit, we’ll take a look at a simple way to create a web service in Raku by using Cro,1 a set of libraries that makes it easy to write asynchronous web clients and services. The name Cro comes from a terrible pun: it allows me to write microservices, my cro services.
Later in this chapter, we’ll take a look at how Cro achieves its declarative API.
12.1 Getting Started with Cro
We’ll reuse the code from Chapter 4, which converts UNIX timestamps into ISO-formatted datetime strings and vice versa, and now expose them through HTTP.
In this example, we see the subroutines route, get, and content that are exported by the modules Cro::HTTP::Router.
route takes a block as an argument and returns an application. Inside the block, we can call get (or other HTTP verb functions such as post or put) to declare routes, pieces of code that Cro calls for us when somebody requests the matching URL through HTTP.
Here, the route declaration starts as get -> 'datetime', Int $timestamp. The -> arrow introduces a signature, and Cro interprets each argument as a part of a slash-delimited URL. In our example, the URL that matches the signature is /datetime/ followed by an integer, like /datetime/1578135634. When Cro receives such a request, it uses the constant string datetime to identify the route and puts the 1578135634 into the variable $timestamp.
The logic for converting the timestamp to a DateTime object is familiar from Chapter 4; the only difference is that instead of using say to print the result to standard output, we use the content function to serve the back to the HTTP requester. This is necessary because each HTTP response needs to declare its content type so that, for example, a browser knows whether to render the response as HTML, as an image, etc. The text/plain content type denotes, as the name says, plain text that is not to be interpreted in any special way.
The code after is classical plumbing: it instantiates a Cro::HTTP::Server object at a given TCP port (here 8080; feel free to change it to your liking) and our collection of one meager route and then tells it to start serving HTTP requests. We chose the host 0.0.0.0 (which means bind to all IP addresses) so that if you run the application in a Docker container, it can be reached from the host. If you do not use Docker, using 127.0.0.1 or localhost is safer, as it doesn’t expose the application to other machines in the network.
The signal() function returns a Supply,3 which is an asynchronous data stream, hereof inter-process communication signals. signal(SIGINT) specifically only emits events when the process receives the INT or interrupt signal, which you can typically create by pressing the keys Ctrl+C in your terminal.
react is usually used in its block form, react { .... } and shortened here because it applies to only one statement. It runs and dispatches supplies in whenever statements until the code calls the done function (or all the streams finish, which doesn’t happen for the signal streams).
So, inside react, whenever signal(SIGINT) { ... } calls the code marked by ..., each time the SIGINT signal is received – in which case we stop the HTTP server and exit the react construct.
All of this is a complicated way to exit the program when somebody presses Ctrl+C.
Due to the asynchronous nature of Cro, you could also do other things here, like processing other supplies in the react block (like period timers, streams of file change events), while the HTTP server is merrily running.
where the --/test option tells zef not to run the module tests, which both take a long time, and require some local infrastructure that you are unlikely to have available.
12.2 Expanding the Service
This defines a second route, under a similar url as before, /datetime/YYYY-MM-DD HH:MM:SS (where the time part is optional). The logic is again copied from Chapter 4, so no surprises here. The only difference is that with the command-line application, the command-line parser split the date and time part for us, which we now explicitly do with a call to .split(' ').
12.3 Testing
Testing a web application can be a bit of a pain sometimes. You have to start the application server, but first you need to find a free port where it can listen, and then you make your requests to the server, and tear it down afterward.
With a tiny bit of restructuring and the Cro::HTTP::Test module, all of this can be avoided.
You can start the HTTP server as before, now with the added benefit of being able to override the port and the host (the IP that the server listens on) through the command line.
We meet our newest friend, sub test-service. We call it with two arguments, the routes to be tested and a block with our tests. Inside this block, the get() routine calls the appropriate routes without any server being started and returns an object of type Cro::HTTP::Test::TestRequest. With the test routine, we can check that this test response fulfills our expectations, here regarding the response code (status) and the JSON response body.
Each call to test produces one test in the output and a subtest (indicated by indentation) for each individual comparison.
12.4 Adding a Web Page
We have our mini web service at a place now where another program can talk to it comfortably through JSON over HTTP, but that’s not really friendly toward end users.
The first one has an empty signature and so corresponds to the / (root) URL and serves the file index.html. The second one serves a file called index.js with the same URL.
The static helper can do more, like serving whole directories while preventing malicious path traversal,5 but for our cases, the simple form is enough.
The visible elements are just some headings, an input form and a button for submitting it, as well as an empty list for the results.
This piece of code uses the jQuery6 library and subscribes to click events on the button. When the button is pressed, it reads the text from the input element, submits it asynchronously toward the URL /datetime/ followed by the input, and appends the result as list items to the unordered list (<ul>).
This is far from perfect, as the web application is missing error handling and visual appeal, but it does illustrate how you can have a pretty machine-focused API endpoint in your application and then put a user interface on top that uses HTML and javascript.
There are several projects that aim to keep the JavaScript code composable and maintainable, like Vue.js,7 angular,8 and React.9 In fact, the Cro documentation comes with a tutorial for building a single page application with React and Redux,10 which you should follow if you want to dive deeper into this subject.
12.5 Declarative APIs
We could now grow our application with more routes, authentication,11 and more, but instead I want to draw attention to how Cro creates its APIs.
get is a function that takes another function as an argument, something we’ve seen in Chapter 12. In contrast to those examples, get doesn’t just call the function it receives; it introspects its signature to figure out when to call it.
Through the .signature.params method chain, we obtain a list of Parameter12 objects representing each function parameter; we can ask them for the type, additional constraints (like the string datetime used on the first parameter), the variable name, and many more properties.
Just like get, route { ... } is a function that calls its argument function after a bit of setup. It sets up a dynamic variable that get latches on to, which enables route to return an object with information about all the calls to get and so all the routes.
Sub dispatcher has to copy the contents of its dynamic variable into a lexical variable my @cases, because the dynamic variable is scoped to the execution time of the function it is declared in, so it ceases to exist after function dispatcher has returned. But dispatcher needs the contents to do its work, iterating through the cases and calling the first one that matches. It does this in an anonymous function that it returns so that the programmer can reuse the matcher in several code locations.
On your first read of code using Cro, you might have thought that the route and get construct looked like language extensions; instead, they turn out to be cleverly named functions that receive other functions as arguments. You can use the same techniques in your own libraries and frameworks to create interfaces that feel natural to the programmer that uses them.
12.6 Summary
With the Cro libraries, you can expose functionality pretty easily through HTTP. First you create some routes (code that is called by Cro when somebody requests the matching URL) in a route { ... } block and then pass the routes to the HTTP server. You start the server, and you’re done.
Each route communicates its response by calling the content function, specifying both the content type and the response body; JSON serialization happens automatically for the appropriate content type.
A user interface can be created through static HTML and JavaScript, possibly with the help of JavaScript application frameworks.
We have also seen how Cro achieves a natural feel for its API by providing higher-order functions (functions that receive other functions as arguments) and perform introspection on the signatures of these functions.