As promised in previous article, we will be using react.js with jsx. For that we should have our server ready.
Today we will be looking at creating the RESTful API's with OpenAPI.
Again we will using Mojolicious for that. Some other prerequisite which we will be using.
- Mojolicious::Plugin::OpenAPI - OpenAPI / Swagger plugin for Mojolicious
- Mojolicious::Plugin::SwaggerUI - Swagger UI plugin for Mojolicious
We will be using the same example mentioned previously.
So without further delay lets get started.
Creating the data config
For multi-line chart -
{
"title": "Number of automobiles sold per day by manufacturer",
"label": {
"domainAxis": "Date",
"rangeAxis": "Numbers of automobiles sold"
},
"data": [
{
"Date": "2020-04-15",
"Honda": 10,
"Toyota": 20,
"Ford": 6,
"Renault": 16
},
{
"Date": "2020-04-16",
"Honda": 3,
"Toyota": 15,
"Ford": 19,
"Renault": 10
},
{
"Date": "2020-04-17",
"Honda": 5,
"Toyota": 8,
"Ford": 12,
"Renault": 6
},
{
"Date": "2020-04-18",
"Honda": 9,
"Toyota": 10,
"Ford": 4,
"Renault": 12
}
]
}
We will be creating one more chart in this example - stacked clustered column chart. We will look into it in more details when we will create the client side. But for now, lets see the input data which is similar to previous one.
{
"title": "Revenues by region",
"label": {
"domainAxis": "Date",
"rangeAxis": "Expenditure"
},
"data": [
{
"Year": "2003",
"Europe": 2.5,
"America": 2.5,
"Asia": 2.1,
"Australia": 1.2,
"Antartica": 0.2,
"Africa": 0.1
},
{
"Year": "2004",
"Europe": 2.6,
"America": 2.7,
"Asia": 2.2,
"Australia": 1.3,
"Antartica": 0.3,
"Africa": 0.1
},
{
"Year": "2005",
"Europe": 2.8,
"America": 2.9,
"Asia": 2.4,
"Australia": 1.4,
"Antartica": 0.3,
"Africa": 0.1
}
]
}
Creating the mojo app
The version I am using for this article is 9.14.
$ mojo generate app MojoReactApp
Running the app -
$ morbo ./script/mojo_app
Web application available at http://127.0.0.1:3000
Now we will modify this app to suit our need.
Writing the specification
For API creation we will follow the OpenAPI specification. We will be using 3.0 version for this.
{
"openapi": "3.0.2",
"info": {
"version": "1.0",
"title": "Mojo React App API",
"description": "This is a sample server for a mojolicious app.",
"contact": {
"name": "Gaurav Rai",
"url": "https://github.com/rai-gaurav"
}
},
"servers": [
{
"url": "/api/v1",
"description": "Version one api"
}
],
"paths": {
"/multi-line-chart": {
"get": {
"summary": "Get multi line chart data",
"tags": ["Chart Data"],
"operationId": "getMultiLineChartData",
"x-mojo-name": "get_multi_line_chart_data",
"x-mojo-to": {
"controller": "LineCharts",
"action": "get_multi_line_chart"
},
"responses": {
"200": {
"description": "Multi Line Chart Response",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"chart_data": {
"type": "object",
"items": {
"type": "object"
}
}
}
}
}
}
}
}
}
},
"/stacked-column-chart": {
"get": {
"summary": "Get stacked column chart data",
"tags": ["Chart Data"],
"operationId": "getStackedColumnChartData",
"x-mojo-name": "get_stacked_column_chart_data",
"x-mojo-to": {
"controller": "ColumnCharts",
"action": "get_stacked_column_chart"
},
"responses": {
"200": {
"description": "Stacked Column Chart Response",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"chart_data": {
"type": "object",
"items": {
"type": "object"
}
}
}
}
}
}
}
}
}
}
}
}
The above specification can also be written in .yaml format also. For now I am using json one. Save it as public/api.json
.
The equivalent in .yaml format is -
---
openapi: 3.0.2
info:
version: '1.0'
title: Mojo React App API
description: This is a sample server for a mojolicious app.
contact:
name: Gaurav Rai
url: https://github.com/rai-gaurav
servers:
- url: "/api/v1"
description: Version one api
paths:
"/multi-line-chart":
get:
summary: Get multi line chart data
tags:
- Chart Data
operationId: getMultiLineChartData
x-mojo-name: get_multi_line_chart_data
x-mojo-to:
controller: LineCharts
action: get_multi_line_chart
responses:
'200':
description: Multi Line Chart Response
content:
application/json:
schema:
type: object
properties:
chart_data:
type: object
items:
type: object
"/stacked-column-chart":
get:
summary: Get stacked column chart data
tags:
- Chart Data
operationId: getStackedColumnChartData
x-mojo-name: get_stacked_column_chart_data
x-mojo-to:
controller: ColumnCharts
action: get_stacked_column_chart
responses:
'200':
description: Stacked Column Chart Response
content:
application/json:
schema:
type: object
properties:
chart_data:
type: object
items:
type: object
- Inside
server
section we are using/api/v1
asurl
. You can provide full url or relative one. If relative, it is resolved against the server where the given OpenAPI definition file is hosted. More info here - There are total two API endpoint
/multi-line-chart
and/stacked-column-chart
for our 2 charts. But support onlyGET
request. - I encourage you to checkout the specification for more details on each and every key.
- Two keys which are specific to Mojolicious are -
x-mojo-name
andx-mojo-to
.x-mojo-to
contains the name of controller and action to perform in case that particular endpoint is requested. Checkout Mojolicious::Plugin::OpenAPI::Guides::OpenAPIv3 for more details.
Using the plugin in Mojolicious App
Lets add the OpenAPI and SwaggerUi plugin in our application.
Inside MojoReactApp.pm
package MojoReactApp;
use Mojo::Base 'Mojolicious', -signatures;
use MojoReactApp::Model::Data;
# This method will run once at server start
sub startup ($self) {
# Load configuration from config file
my $config = $self->plugin('NotYAMLConfig');
# Configure the application
$self->secrets($config->{secrets});
# Load the "api.json" specification
# Can also be written as -
# "OpenAPI" => {spec => $self->static->file("api.json")->path}
$self->plugin(
"OpenAPI" => {
url => $self->home->rel_file("public/api.json")
}
);
$self->plugin(
SwaggerUI => {
route => $self->routes()->any('api'),
url => "/api/v1",
title => "My Mojolicious App"
}
);
# Helper to lazy initialize and store our model object
$self->helper(
model => sub ($c) {
state $data = MojoReactApp::Model::Data->new();
return $data;
}
);
# Router
my $r = $self->routes;
# Normal route to controller
$r->get('/')->to(template => 'home/welcome');
}
1;
In case you want to use .yaml format, you can use that instead of .json
$self->plugin(
"OpenAPI" => {
url => $self->home->rel_file("public/api.yaml")
}
);
Here I have just used the OpenAPI and SwaggerUI plugin.
We are using the url
to load the specification which have created before.
SwaggerUI is just a fancy thing I am using. There is no such limitation to used it for creating the API.
-
route
- The ui will be available on http://{hostname}:{port}/api -
url
- Url for the specification. Inapi.json
insideserver
section we have written the url as/api/v1
meaning our specification is available under that path. Hence we have given the same path here. - Please have a look at the options for the meaning of each parameters.
- If case you noticed, we have created just one route
/
which will just show the home page. Since we are crating Rest API's and all the other endpoint are already taken care inapi.json
we don't have to add it here. - Our directory structure is also simpler.
📦mojo_react_app
┣ 📂etc
┃ ┣ 📜input_multi_line_chart_data.json
┃ ┗ 📜input_stacked_clustered_column_chart.json
┣ 📂lib
┃ ┣ 📂MojoReactApp
┃ ┃ ┣ 📂Controller
┃ ┃ ┃ ┣ 📜ColumnCharts.pm
┃ ┃ ┃ ┗ 📜LineCharts.pm
┃ ┃ ┗ 📂Model
┃ ┃ ┃ ┗ 📜Data.pm
┃ ┗ 📜MojoReactApp.pm
┣ 📂public
┃ ┗ 📜api.json
┣ 📂script
┃ ┗ 📜mojo_react_app
┣ 📂t
┃ ┗ 📜basic.t
┣ 📂templates
┃ ┣ 📂home
┃ ┃ ┗ 📜welcome.html.ep
┃ ┗ 📂layouts
┃ ┃ ┗ 📜default.html.ep
┗ 📜mojo_react_app.yml
- The
public
section only containsapi.json
. There is no need of css and js as we are creating only API. Even you can remove the wholetemplate
section also. I have just added it for/
endpoint. Thewelcome
template is just some minor adjustment to default one.
% layout 'default';
% title 'Welcome';
<h2>Hello there</h2>
<p>
This is the home page.
For API <%= link_to 'click here' => '/api' %>.
</p>
- Since we are decided to created 2 charts, we have 2 json file in
etc
dir. - Each one has there own controller.
- Our Model is almost similar to previous article with minor modification.
package MojoReactApp::Model::Data;
use strict;
use warnings;
use experimental qw(signatures);
use Mojo::JSON qw(decode_json);
sub new ($class) {
my $self = {};
bless $self, $class;
return $self;
}
sub _read_json_file ($self, $json_file) {
open(my $in, '<', $json_file) or $self->app->log->error("Unable to open file $json_file : $!");
my $json_text = do { local $/ = undef; <$in>; };
close($in) or $self->app->log->error("Unable to close file : $!");
my $config_data = decode_json($json_text);
return $config_data;
}
sub get_line_data ($self) {
my $data_in_json = $self->_read_json_file("etc/input_multi_line_chart_data.json");
return $data_in_json;
}
sub get_column_data ($self) {
my $data_in_json = $self->_read_json_file("etc/input_stacked_clustered_column_chart.json");
return $data_in_json;
}
1;
Controller
Lets create our 2 controller.
Inside lib\MojoReactApp\Controller\ColumnCharts.pm
package MojoReactApp::Controller::ColumnCharts;
use Mojo::Base 'Mojolicious::Controller', -signatures;
use Mojo::JSON qw(encode_json);
sub get_stacked_column_chart ($self) {
# Do not continue on invalid input and render a default 400 error document.
my $app = $self->openapi->valid_input or return;
my $data_in_json = $app->model->get_column_data();
# $output will be validated by the OpenAPI spec before rendered
my $output = {chart_data => $data_in_json};
$app->render(openapi => $output);
}
1;
Simple and easy!!. Here we are validating the input in $self->openapi->valid_input
. After that we are getting the data from model and just returning that as the response inside the hash.
Similarly for lib\MojoReactApp\Controller\LineCharts.pm
package MojoReactApp::Controller::LineCharts;
use Mojo::Base 'Mojolicious::Controller', -signatures;
use Mojo::JSON qw(encode_json);
sub get_multi_line_chart ($self) {
# Do not continue on invalid input and render a default 400 error document.
my $app = $self->openapi->valid_input or return;
my $data_in_json = $app->model->get_line_data();
# $output will be validated by the OpenAPI spec before rendered
my $output = {chart_data => $data_in_json};
$app->render(openapi => $output);
}
1;
Exact similar to previous one, except we are calling different model function here.
Lets run it and see the output.
Hitting 'http://localhost:3000/'
You can see your home page which get generated by the welcome
template. Click on the link or hit 'http://localhost:3000/api' in browser, you can see the shiny Swagger UI.
We can also see the 2 endpoint which you have created.
We can see all the things which we have written in api.json
This swagger ui is ultimately using that specification and generating this page.
Hit 'http://localhost:3000/api/v1' and you can see the specifications also.
Now lets try to do a GET request on one of the endpoint. You can use the SwaggerUI for this or a normal curl request or directly hit the endpoint from browser or use some 3rd party tool like Postman , all will work.
SwaggerUI will just give you some documentation about usage and the expected response.
Let hits the /multi-line-chart
endpoint. In SwaggerUi page, click on 'Try it out' and 'execute' it.
You can see the response in JSON format.
We can see there is equivalent curl request already prepared which we can also use to get the same response.
Also in 'Requested URL' we can see ultimately it is calling the '/api/v1/multi-line-chart' which you can hit from browser and see the exact result.
With this our API development is done. Next we will be seeing how to use this API in React.js and create those charts in jsx.
Also, I have used morbo
which is good for development. But for production we need to make certain changes. I will talk about those (Docker, Makefile, Apache2/Nginx, uWSGI/Plack and hypnotoad) in a different section.
The above example is available at github.
There is also a good tutorial available on Mojolicious blog - A RESTful API with OpenAPI
Perl onion logo taken from here
Mojolicious logo taken from here
OpenAPI logo taken from here
Top comments (7)
Question: your write that the OpenAPI spec can be written in YAML as well. While this is true according to the OpenAPI rules, the
Mojolicious::Plugin::OpenAPI
plugin only accepts JSON, AFAIK.Or am I missing some hidden feature?
Since you mentioned, I also tried to look at documentation of
Mojolicious::Plugin::OpenAPI
and I found only one reference of yaml. Also I haven't found any example of yaml. Maybe the documentation can be improved with some trivial example in .yaml format also.I tried to use the yaml equivalent in my current code and its working. I have update it above.
Mojolicious::Plugin::OpenAPI
is internaly using YAML::XS and I have found it in this version.It also internally use JSON::Validator which support following schema format.
So, yes the plugin support .yaml format file also.
Yep, thanks. I just tried with another app I have. I recall trying before and the plugin was throwing errors. But I guess that was more due to the way I converted JSON to YAML. Syntax errors in a YAML spec are well-reported. Schema errors OTOH are much less clear in the YAML case.
Keen to understand better OpenApi as well as being fairly new to Mojolicious and React.js, I found your presentation superb. Though these improvement/corrects would help the presentation:
For Part1 (in MojoReactApp.pm):
$r->get('/')->to(template => 'home/welcome');
change to:
$r->get('/')->to(controller => 'Example', action => 'welcome');
In Part4:
"proxy": "https://localhost",
change to (as per your Git repository):
"proxy": "http://localhost:3001",
import { withRouter } from "react-router";
change to:
import { withRouter } from "react-router-dom";
add after 'import Home from "./components/Home";' these two lines:
{/*import Chart1 from "./components/Chart1";
import Chart2 from "./components/Chart2"; */}
which will be also unblocked where you mention to do so at the start of "Creating charts".
Excellent, I have found how I will invest my weekend. Thanks.
Thank you @raigaurav !
Question 1: In your
Model::Data
, I don't see how$self->app->log->error("error");
will ever work. Here$self
refers to the model, not the app.Question 2: Do you know how to support multiple routes? For example if your API definition contained multiple servers, for example:
Great article 👍