Today’s post is about a feature that was added on Koha 18.11: the ability to add API routes using plugins.
In earlier posts we added a table, used DBI to perform CRUD operations on the controller script. Later we added a cool Koha::Object(s) based class to abstract those table operations.
This post will try to highlight how easy is to add routes on top of Koha::Object-based classes.
API plugin
In order for a plugin to be considered for adding API routes, it needs to implement two methods:
- api_namespace
- api_routes
The plugin system will completely skip plugins that don’t implement both.
api_namespace
API routes look pretty much like /api/v1/path/to/a/route
. It is easy to notice that plugins have lots of flexibility
to add routes, so we need a way to avoid route collissions or overwrites.
Plugin routes are always added inside a separate /contrib
path, so they can never overwrite those from the core distribution.
Then, the api_namespace comes into play to avoid plugins overwriting each other’s routes. In our example the method looks like:
sub api_namespace {
my ($self) = $_;
return 'fancy';
}
In this case, the resulting base path for the routes will be /api/v1/contrib/fancy
.
There can still be collissions (i.e. two plugins using the same namespace), Koha will keep the first plugin implementing a namespace, and skip the rest of them.
api_routes
Koha implements a RESTful API using the OpenAPI v2 specification language. The spec is written in JSON. You will need some time to get used to the structure if you are not familiar already.
When it comes to plugins, they only introduce new paths, so you can focus on that part of the standard. I always find the OpenAPI map to be the best reference when writing API routes.
In order to tell Koha the paths the plugin is adding, you need to implement a method that returns that said paths definition:
sub api_routes {
my ($self) = $_;
return <<SPEC;
{
"/words": {
"get": {
"x-mojo-to": "FancyPlugin::Controller#list",
"operationId": "listFancyWords",
"tags": [
"fun"
],
...
SPEC
}
When you get into writing API routes, you will notice this is not really handy:
- The spec can get lenghty, making the file unreadable.
- As this is a Perl file with inline JSON, editors won’t work for syntax highlighting or showing errors.
- Same as above, but for linting the spec.
What we do is taking advantage of the mbf_read
method plugins implement, and put the spec on a separate bundle file:
use Mojo::JSON qw(decode_json);
sub api_routes {
my ($self) = @_;
my $spec_str = $self->mbf_read('openapi.json');
my $spec = decode_json($spec_str);
return $spec;
}
Controller class
If you look at the openapi.json file, you will notice an important entry on each verb, on each path: x-mojo-to
. That attribute is used by Mojolicious to route the requests to the implementing methods:
"/words": {
"get": {
"x-mojo-to": "FancyPlugin::Controller#list",
"operationId": "listFancyWords",
"tags": [
"fun"
],
...
}
}
The class name resolution step will append Koha::Plugin::
to the class name, resulting in Koha::Plugin::FancyPlugin::Controller
, and the list() method being responsible of implementing the route.
Vendors usually add their names to the class name, or just name their classes longer, like in Koha::Plugin::Com::Theke::INNReach::CircController
. In this case, for a borrowerrenew controller method, the x-mojo-to
will look like:
"x-mojo-to": "Com::Theke::INNReach::CircController#borrowerrenew"
Testing the API
Once we’ve got everything in place, it is time to check things are working. As a start, you need to restart Plack. You might also need to re-install the plugin:
$ kshell
$ perl misc/devel/install_plugins.pl
...
Installed Our fancy plugin version {VERSION}
All plugins successfully re-initialised
Uncaught exceptions on the API are usually reported to the /var/log/koha/kohadev/api-error.log
file. And warnings and some other error reporting in /var/log/koha/kohadev/plack-error.log
.
The sample API we implemented for the FancyPlugin implements the following routes:
GET /api/v1/contrib/fancy/words
GET /api/v1/contrib/fancy/words/{fancy_word_id}
POST /api/v1/contrib/fancy/words
PUT /api/v1/contrib/fancy/words/{fancy_word_id}
DELETE /api/v1/contrib/fancy/words/{fancy_word_id}
You should be able to see them documented under http://kohadev.myDNSname.org:8080/api/v1/.html. If you don’t find them, look at the logs while restarting Plack to see what’s being reported.
For showing you how to try the API, I’ll be using Postman which is a really cool tool, available for most operating systems. There are other tools, extensions for Firefox, etc. I found Postman to be the easiest to use, specially when dealing with OAuth2.
For the purpose of this tests, I have enabled the RESTBasicAuth system preference. And I use a regular login an password combination.
Let’s try to retrieve the current fancy words:
an empty list is expected as we haven’t added any fancy words yet. Let’s add one:
Notice a fancy_word_id and timestamp have been assigned and returned. Let’s try using the fancy_word_id to retrieve a single fancy word:
Now add some more words, and repeat the first action to fetch them all:
Let’s say we want to update one of the words because we want another one:
And now delete a word:
If we try to retrieve the deleted word, it should tell us the resource doesn’t exist:
And that’s it! We’ve got full CRUD operations on fancy words. Later we will show how to use the API from the UI, replacing old-style controllers, for API-driven interactions!