Tutorial on Mojolicious::Plugin::OpenAPI: Hello World

I have always wanted to get my hands dirty with Swagger. I recently fell over Mojolicious::Plugin::OpenAPI, which fits into my boring stack and I decided to do a prototype.

I followed the tutorial for Mojolicious::Plugin::OpenAPI and found it a bit confusing, so I decided to write up a more simple tutorial.

This tutorial requires that you have Mojolicious installed and recommends carton. The installation of these components is however beyond the scope of this tutorial.

OpenAPI comes from Swagger, which I have had a look at, much water has run under that bridge, so now it is time to look at OpenAPI a specification on how to write RESTful APIs in a standardised format.

Here goes, lets start with a basic hello world example, all files are available on GitHub.

Hello World

First we set up an application, yes we could do a Mojolicious lite-app, but I primarily use Mojolicious apps, so I think it makes sense to keep stick to this for reference.

$ mojo generate app HelloWorld

Jump into our newly generated application directory

$ cd hello_world

We then install the plugin we need to enable OpenAPI in our Mojolicious application

Using CPAN shell:

$ perl -MCPAN -e shell install Mojolicious::Plugin::OpenAPI

Using cpanm:

$ cpanm Mojolicious::Plugin::OpenAPI

If you need help installing please refer to the CPAN installation guide.

Create a definition JSON file based on OpenAPI to support an Hello World implementation based on the OpenAPI specification:

$ touch openapi.conf

The exact name of this file is insignifcant, I just prefer to have clear and understandable filenames for easy identification.

Open openapi.conf and insert the following snippet:

{
    "swagger": "2.0",
    "info": { "version": "1.0", "title": "Hello World example" },
    "basePath": "/api",
    "paths": {
      "/hello_world": {
        "get": {
          "operationId": "helloWorld",
          "x-mojo-name": "hello_world",
          "x-mojo-to": "example#hello_world",
          "summary": "Example app returning hello world",
          "responses": {
            "200": {
              "description": "Returning string 'hello world'",
              "schema": {
                "type": "object",
                "properties": {
                    "greeting": {
                        "type": "string"
                    }
                }
              }
            },
            "default": {
              "description": "Unexpected error",
              "schema": {}
            }
          }
        }
      }
    }
}

Now lets go over our definiton.

  • basePath: defines the root of our URL, so we would be able to access our application at /api, recommendations on versioning APIs using this part is do exist, but for our example application, this is out of scope.

  • paths: here we define our first API path, so our Hello World application can be accessed at: /api/hello_world

  • operationId: the is an operation identifier, it is important for the OpenAPI part, whereas the two following definitions are mappings of the same operation identifier towards the Mojolicious application

  • x-mojo-name: this is the name used to identify our operation in the Mojolicious application

  • x-mojo-to: this is the specification for the route to be used for our operation in the Mojolicious application, more on this later

  • responses: here we define the type we want to handle, for now we settle for 200. The response definition outline our response, this could be boiled down to a string instead of an object, with properties, but the example would be come too simple and in my opinion we work primarily with objects over basic types, so this extended example makes for a better reference.

Next step is to enable the MetaCPAN: Mojolicious::Plugin::OpenAPI plugin in the application

Open the file: lib/HelloWorld.pm and add the following snippet:

$self->plugin("OpenAPI" => {url => $self->home->rel_file("openapi.json")});

Note the pointer to our previously created file: openapi.json.

The complete file should look like the following:

package HelloWorld;
use Mojo::Base 'Mojolicious';

# This method will run once at server start
sub startup {
  my $self = shift;

  $self->plugin('OpenAPI' => {url => $self->home->rel_file('openapi.json')});

  # Load configuration from hash returned by "my_app.conf"
  my $config = $self->plugin('Config');

  # Documentation browser under "/perldoc"
  $self->plugin('PODRenderer') if $config->{perldoc};

  # Router
  my $r = $self->routes;

  # Normal route to controller
  $r->get('/')->to('example#welcome');
}

1;

Then we add the actual operation, open the file: lib/HelloWorld/Controller/Example.pm and add the following snippet:

sub hello_world {
    my $c = shift->openapi->valid_input or return;

    my $output = { greeting => 'Hello World' };
    $c->render(openapi => $output);
}

Note that this maps to the definition in our API definition: openapi.conf

"x-mojo-to": "example#hello_world",

The complete file should look like the following:

package HelloWorld::Controller::Example;
use Mojo::Base 'Mojolicious::Controller';

# This action will render a template
sub welcome {
  my $self = shift;

  # Render template "example/welcome.html.ep" with message
  $self->render(msg => 'Welcome to the Mojolicious real-time web framework!');
}

sub hello_world {
    my $c = shift->openapi->valid_input or return;

    my $output = { greeting => 'Hello World' };
    $c->render(openapi => $output);
}

1;

I decided to implement the tutorial in a scaffolded application, you could create your own controller, but changing an existing controller this way demonstrates how our newly added OpenAPI API end-point, can live in unison with existing and additional end-points.

Now start the application

$ morbo script/hello_world

And finally - lets call the API, do note you do not need jq and your could use curl or httpie, so this is just for sticking to the already available tools, jq being the exception.

$ mojo get --verbose http://localhost:3000/api/hello_world | jq
GET /api/hello_world HTTP/1.1
Host: localhost:3000
User-Agent: Mojolicious (Perl)
Content-Length: 0
Accept-Encoding: gzip

HTTP/1.1 200 OK
Server: Mojolicious (Perl)
Content-Length: 26
Date: Fri, 27 Jul 2018 08:47:33 GMT
Content-Type: application/json;charset=UTF-8

{
  "greeting": "Hello World"
}

Yay! and our first Mojolicious OpenAPI implementation works!

In addition to the operation, you can obtain the specification by calling the following URL: /api

$ mojo get http://localhost:3000/api/

And as mentioned earlier our existing operations and parts of the application still works as expected, try calling the URL: /

$ mojo get http://localhost:3000/

That is it for now, good luck with experimenting with Mojolicious OpenAPI integration and OpenAPI. Thanks to Jan Henning Thorsen (@jhthorsen) for the implementation of Mojolicious::Plugin::OpenAPI.

References