How to easily add a GraphQL interface to the public REST API of your Mojolicious application

During the Mojolicious 2017 Advent calendar series, we looked at how to build a public REST API using Mojolicious. A technology that is getting a lot of buzz is GraphQL. Now that it has been ported to Perl 5, what if there were an easy way to let people access your API using it?

Why use GraphQL

GraphQL could be considered an evolution of OpenAPI. It not only allows, but forces you to clearly specify all of your types, inputs and outputs, and available queries and mutations, in a simple, clear way. When you make actual queries, you request everything that you want to get back, and you get back all of that, with no unnecessary extras, and with just one request. This saves both time and network bandwidth.

To learn more about how to use it, there is a translation of the JavaScript tutorial for GraphQL into Perl using Mojolicious.

Prerequisites

To run the example code below, you need to have gone through the "build a public REST API" article and installed its prerequisites, and made your app. Then install the following modules:

$ cpanm Mojolicious::Plugin::GraphQL      # server side plugin
$ cpanm GraphQL::Plugin::Convert::OpenAPI # convert OpenAPI spec to GraphQL

How to add GraphQL to your existing REST API

To do the bare minimum to get our miniature API going with GraphQL, we just need to change lib/MyApp.pm a little:

# add this at the top:
use Mojo::File 'path';

# change the `OpenAPI` line to:
my $path = $self->static->file("api.yaml")->path;
my $api = $self->plugin(OpenAPI => {spec => $path});
$self->plugin(GraphQL => {convert => [ qw(OpenAPI), $api->validator->bundle, $self ], graphiql => 1});

This gets the plugin object returned by the OpenAPI plugin, then uses that to pass the API spec along to the GraphQL plugin. The graphiql flag is to tell the plugin to deliver the "GraphiQL" interface if you request the /graphql interface with your browser. We'll talk more about that soon.

We'll also add a tiny test this works to t/basic.t:

$t->post_ok('/graphql', json => {
  query => 'mutation m {echo(body: [{key:"one", value:"two"}]) { key value }}'
})->json_is(
  { data => { echo => [{key=>"one", value=>"two"}] } },
)->or(sub { diag explain $t->tx->res->body });

Check it still all works right:

$ prove -l t

This makes a GraphQL request as conventionally done over HTTP. There are two points to note:

  • it's a "mutation", which probably sounds like it would change something, but here it doesn't - it's still just an "echo" service
  • both the inputs and outputs look like hashes with key and value attributes

The reason it's a mutation is because our basic REST API used a POST route so it could pass in an arbitrary JSON object. The GraphQL plugin treats all routes that aren't a GET as mutations. It also turns such an object into a list of "hash pairs", which are turned to/from a real hash before passing to/getting back from the REST API.

Let's add a slightly more realistic REST API, then we'll be able to use Facebook's excellent "GraphiQL" tool to talk to our API!

Adjusting the REST API specification

We're going to add a GET echo route, and a fake user-creation POST route, including the controller code and tests. We'll also add a User definition, which will be turned into a GraphQL type of the same name (the other "pairs" stuff gets turned into types as well). For simplicity, we'll modify our index so it works as the user-creating code in the Echo controller, but in real life you should already see how you'd divide different things into different modules for easier maintenance.

Make your api.yaml read:

swagger: '2.0'
info:
  version: '0.42'
  title: Dummy example
schemes: [ http ]
basePath: "/api"
paths:
  /echo:
    post:
      x-mojo-to: "echo#index"
      operationId: echo
      parameters:
      - in: body
        name: body
        schema:
          type: object
      responses:
        200:
          description: Echo response
          schema:
            type: object
    get:
      x-mojo-to: "echo#index"
      operationId: echoGet
      parameters:
      - in: query
        name: q
        type: string
      responses:
        200:
          description: Echo response
          schema:
            type: string
  /user:
    post:
      x-mojo-to: "echo#index"
      operationId: createUser
      parameters:
      - in: body
        name: user
        schema:
          $ref: "#/definitions/User"
      responses:
        200:
          description: Created User
          schema:
            $ref: "#/definitions/User"
definitions:
  User:
    type: object
    properties:
      name:
        type: string
      email:
        type: string

Now change your lib/MyApp/Controller/Echo.pm so the index method is:

sub index {
  # Validate input request or return an error document
  my $self = shift->openapi->valid_input or return;
  my $data = $self->req->method eq 'POST'
    ? $self->req->json
    : $self->req->param('q');
  # Render back the same data as you received using the "openapi" handler
  $self->render(openapi => $data);
}

Of course you'll want to update your tests, so add this to t/basic.t:

$t->get_ok('/api/echo?q=good')->status_is(200)->json_is("good");
$t->post_ok('/api/user', json => {email=>'a@b',name=>'Bob'})->json_is(
  {email=>'a@b', name=> 'Bob'}
);
$t->post_ok('/graphql', json => {
  query => '{echoGet(q: "Hello")}'
})->json_is(
  { data => { echoGet => 'Hello' } },
)->or(sub { diag explain $t->tx->res->body });
$t->post_ok('/graphql', json => {
  query => 'mutation m {createUser(user: {email:"one@a", name:"Bob"}) { email name }}'
})->json_is(
  { data => { createUser => {email=>'one@a', name=>'Bob'} } },
)->or(sub { diag explain $t->tx->res->body });

By now you know how to run the tests!

How to use GraphiQL to interactively talk to your API

Start up your service:

$ ./script/my_app daemon -l 'http://*:5000'

And connect your browser to http://localhost:5000/graphql and try this query:

{echoGet(q: "hi")}

Now do a fake "user creation":

mutation m {
  createUser(user: {
    email: "one@two"
    name: "Jan"
  }) {
    email
    name
  }
}

For further exploration, use the "Docs" button in the top-right of the window. It will show you the available types, starting from the root Query (whose fields are all the queries), and Mutation, which has our createUser with input that's a UserInput and output that's a User.

See also

Did you find GraphQL interesting? Check out these resources to find out more:

Image by Matt Howard CC BY-SA 2.0

Tagged in : api, openapi, swagger, graphql

author image
Ed J

Ed J (aka "mohawk" on IRC) has been using Perl for a long time. He is currently porting the reference GraphQL implementation from the JavaScript version to Perl. Find out more by joining the #graphql-perl channel on irc.perl.org!