Your first Pragma API

Pragma provides all the components you need to build beautiful, functional RESTful APIs. This guide will show you how you can use them to build a simple API for a blog engine.

Introduction

Pragma is split into four components:

  • Operation: operations encapsulate the business logic of your API. Pragma delegates to operations when an endpoint of your API is called.
  • Contract: contracts are like form objects, but for APIs. They also validate incoming GET parameters to ensure that the requests are well-formed.
  • Decorator: decorators (sometimes also referred to as "representers") take your resources and present them to your API's clients in JSON or other formats.
  • Policy: policies are lightweight Ruby classes that determine which resources a user has access to and what operations they can perform on it.

We also provide additional tools:

  • Pragma::Rails is our Rails integration. It's a very thin layer taking care of dispatching to operations with the right parameters.
  • Pragma Rails starter is our starter Rails template. It comes with Pragma (obviously!) plus some additional tools that will make API development a breeze.

In this guide, we'll leverage all of the aforementioned gems and tools to build a simple RESTful API for a blog engine with Pragma and Rails. Our API will allow users to list, display, create, update and delete articles. We will also implement a dead-simple authentication system to showcase Pragma's authorization system.

Bootstrapping the app

First things first. To create a new Rails app with Pragma support, just clone our starter repository:

$ git clone https://github.com/pragmarb/pragma-rails-starter.git blog

This will create a Pragma app ready to go in the blog directory. Just enter the directory and take a look at the readme to learn about all the tools it comes with.

Since we need our API to persist data, we'll create first create our models:

$ rails g model Author first_name last_name
$ rails g model Article author:belongs_to title body:text published:boolean
$ rails db:migrate

Creating resources

A Pragma resource is nothing more than data which can be operated on, just like a REST resource. On a technical level, each resource will have its own namespace and contain operations, decorators, contracts and a policy. All of these are optional, but usually you'll want to have all of them.

In a Rails Pragma app, your resources will be stored in app/resources. They will usually map to a model, but not always. Pragma::Rails also provides a generator for creating a new resource, complete with its own system tests. Think of it as rails g scaffold for Pragma. Try this:

$ rails g pragma:resource article

This will generate the following file structure:

app/resources
└── api
    └── v1
        └── article
            ├── contract
            │   ├── base.rb
            │   ├── create.rb
            │   └── update.rb
            ├── decorator
            │   ├── collection.rb
            │   ├── instance.rb
            ├── operation
            │   ├── index.rb
            │   ├── show.rb
            │   ├── create.rb
            │   └── update.rb
            │   └── destroy.rb
            └── policy.rb
spec/requests
└── api
    └── v1
        └── articles_spec.rb

It will also edit your config/routes.rb to create CRUD routes for the resource.

You are encouraged to explore the generated files, even though most of them will be empty at this point. You have created your first resource which is directly mapped to a model, but it doesn't accept any input or present any output yet. It's also publicly accessible, meaning everyone will be able to see all articles, even the unpublished ones.

Let's see how we can fix all of that!

Presenting data

Let's start with the basics: reading existing articles. First of all, we'll create a new author and article from the console:

$ rails c

When the console is open, type the following into it:

author = Author.create!(
  first_name: 'John',
  last_name: 'Doe'
)

Article.create!(
  author: author,
  title: 'Hello, world!',
  body: 'This is my first article. How cool is this?'
)

Now, let's try to access our articles:

$ curl 'http://localhost:5000/api/v1/articles/'

Here's what we get:

{
  "data": [],
  "total_entries": 0,
  "per_page": 30,
  "total_pages": 1,
  "previous_page": null,
  "current_page": 1,
  "next_page": null,
  "type": "list"
}

What's going on here? Well, the instance decorator for your Article resource is empty, so Pragma is responding with an empty JSON object. Decorators take your resource instances and represent them as JSON. Each resource usually has two decorators: one for presenting the collection of resources and one for presenting a single instance, named respectively Collection and Instance.

Let's edit the instance decorator, so that we can present articles properly:

module API
  module V1
    module Article
      module Decorator
        class Instance < Pragma::Decorator::Base
          feature Pragma::Decorator::Association
          feature Pragma::Decorator::Timestamp
          feature Pragma::Decorator::Type

          property :id
          property :title
          property :body
          property :published
          property :author_id, as: :author

          timestamp :created_at
          timestamp :updated_at
        end
      end
    end
  end
end 

As you can see, you can present a property as-is with property. We have also renamed our author_id property to author, so that it can be easily expanded in the future (more on this in the following guides).

timestamp, on the other hand, is a helper that takes a datetime object and converts it into a UNIX timestamp, which is the preferred method to present times in an API.

If you try to access the articles again, it will look much better now:

 {
  "data": [{
    "type": "article",
    "id": "be9da202-fc8d-44e6-a8f7-54b1398cfad0",
    "title": "Hello, world!",
    "body": "This is my first article. How cool is this?",
    "published": false,
    "author": "bad87963-d8b2-408a-876e-c84740121aed",
    "created_at": 1509238434,
    "updated_at": 1509238434
  }],
  "total_entries": 1,
  "per_page": 30,
  "total_pages": 1,
  "previous_page": null,
  "current_page": 1,
  "next_page": null,
  "type": "list"
}

All the meta-data about the collection (the number of entries, the pagination information and so on) is provided by Pragma's default Collection decorator, which looks like this by default:

module API
  module V1
    module Article
      module Decorator
        class Collection < Pragma::Decorator::Base
          feature Pragma::Decorator::Type
          feature Pragma::Decorator::Collection
          feature Pragma::Decorator::Pagination

          decorate_with Instance
        end
      end
    end
  end
end

You will almost never have to edit this, unless you want to add additional metadata about the collection.

Decorators are built on top of Representable. They provide conditional properties, association expansion, type casting and many other powerful features. For a comprehensive list, have a look at Decorator's documentation.

Accepting input

Let's start with the basics: creating a new article. What we want to do is get the following cURL request to work properly:

curl 'https://our-api.example.com/api/v1/articles' \
  -X POST \
  -H 'Content-Type: application/json' \
  -d '{
    "title": "Hello, world!",
    "body": "This is my first article. How cool is this?",
    "author": "bad87963-d8b2-408a-876e-c84740121aed"
  }'

In order to do that, we will edit our resource's contract. A contract is a class whose purpose is to validate the input to the resource's API endpoints and persist it to the underlying model.

For our purpose, we will open the API::V1::Article::Contract::Base class and edit it like this:

module API
  module V1
    module Article
      module Contract
        class Base < Pragma::Contract::Base
          property :title, type: coercible(:string)
          property :body, type: coercible(:string)
          property :published, type: form(:bool)
          property :author

          validation do
            required(:title).filled
            required(:body).filled(min_length?: 100)
            required(:author).filled
          end

          def author=(val)
            super ::Author.find_by(id: val)
          end
        end
      end
    end
  end
end

You might not believe it, but that's the only thing you need to do to get that cURL request to pass. Try it if you don't believe me!

Let's dissect what that code means though. There are three parts to our contract:

  1. The contract's properties or attributes, which in turn determine which aspects of the resource can be manipulated through the API. You can see that we are also coercing both our properties to strings, so that we can safely operate on them.
  2. The contract's validations. In our case, we want title, body and author to be filled, and the body to be at least 100 characters in length. If any of these validations fail, the article will not be persisted and the API will respond with the 422 Unprocessable Entity status code and a list of huamn-readable validation errors, organized by property.
  3. The author= method. This is a custom setter/coercion method that will be used to convert the author's UUID into the relevant Author object. This serves two purposes: first, it allows us to use author both when presenting output and when accepting input, and second, it lets us easily validate that a valid author ID has been passed. In case you're wondering, we use find_by instead of find because we want to fail gracefully in case the ID is not valid.

Contracts are built on top of Reform, so whatever you can do with Reform, you can do with contracts. Coercion is provided by Dry::Types, while validation is provided by Dry::Validation. We urge you to read the documentation of all these gems and of Pragma::Contract to learn how to make the most of them.

Restricting access

Now, let's try to restrict access to articles. If you remember, our Article model has an author relationship. We want to put the following restrictions in place:

  • authors should be able to completely manage all of their articles, even when unpublished;
  • regular users should only be able to read published articles.

Implementing an authentication system is outside of the scope of this guide, so for now just add this to your application_controller.rb:

class ApplicationController < ActionController::Base
  # ...

  protected

  def current_user
    Author.find(request.headers['X-Author-Id'])
  end
end

(Don't do this at home, please.)

Now, if you open the Policy class for your Article resource, you should see this empty policy:

module API
  module V1
    module Article
      class Policy < Pragma::Policy::Base
        class Scope < Pragma::Policy::Base::Scope
          def resolve
            scope.all
          end
        end

        def show?
          true
        end

        def create?
          true
        end

        def update?
          true
        end

        def destroy?
          true
        end
      end
    end
  end
end

As you can see, currently everyone can do everything, which is not good. Let's tweak the policy a bit to keep hackers away:

module API
  module V1
    module Article
      class Policy < Pragma::Policy::Base
        class Scope < Pragma::Policy::Base::Scope
          def resolve
            scope.where(author: user).or(scope.where(published: true))
          end
        end

        def show?
          record.published? || record.author == user
        end

        def create?
          record.author == user
        end

        def update?
          record.author == user
        end

        def destroy?
          record.author == user
        end
      end
    end
  end
end

This is pretty simple: basically, the first thing we do is define a "scope", which is a class whose purpose is to take a query and a user and refine the query so that it only includes records accessible by that user. In the rest of the policy we define the conditions that the record and user must meet in order for each action to be permitted.

If we try to access the articles at /api/v1/articles now, we'll get an empty collection again:

{
  "data": [],
  "total_entries": 0,
  "per_page": 30,
  "total_pages": 1,
  "previous_page": null,
  "current_page": 1,
  "next_page": null,
  "type": "list"
}

This is because the only article we have is unpublished and we're not the authors. Direct access won't work either:

$ curl 'http://localhost:5000/api/v1/articles/be9da202-fc8d-44e6-a8f7-54b1398cfad0'

This will respond with a 403 Forbidden and the following JSON:

{
  "error_type": "forbidden",
  "error_message": "You are not authorized to access this resource."
}

Let's see if authenticating as the article's author, by passing the X-Author-Id header, gives us access:

$ curl 'http://localhost:5000/api/v1/articles' \
  -H 'X-Author-Id: bad87963-d8b2-408a-876e-c84740121aed'

We're in again:

 {
  "data": [{
    "type": "article",
    "id": "be9da202-fc8d-44e6-a8f7-54b1398cfad0",
    "title": "Hello, world!",
    "body": "This is my first article. How cool is this?",
    "published": false,
    "author": "bad87963-d8b2-408a-876e-c84740121aed",
    "created_at": 1509238434,
    "updated_at": 1509238434
  }],
  "total_entries": 1,
  "per_page": 30,
  "total_pages": 1,
  "previous_page": null,
  "current_page": 1,
  "next_page": null,
  "type": "list"
}

If we publish our article by updating the published property to true we won't need to authenticate anymore. Let's run the following in the Rails console:

Article.first.update! published: true

Now we'll attempt access as guests once more:

$ curl 'http://localhost:5000/api/v1/articles'

This time, it works perfectly:

 {
  "data": [{
    "type": "article",
    "id": "be9da202-fc8d-44e6-a8f7-54b1398cfad0",
    "title": "Hello, world!",
    "body": "This is my first article. How cool is this?",
    "published": false,
    "author": "bad87963-d8b2-408a-876e-c84740121aed",
    "created_at": 1509238434,
    "updated_at": 1509238434
  }],
  "total_entries": 1,
  "per_page": 30,
  "total_pages": 1,
  "previous_page": null,
  "current_page": 1,
  "next_page": null,
  "type": "list"
}

These are the basics of policies. There's nothing terribly fancy about them, but you are encouraged to take a look at the GitHub repo and play around with the code.

Tweaking operations

Now that we are aware of who the current user is, you'll probably have noticed the inefficiency of having to specify the author ID manually when creating a new article. Instead, we could just take the current user's ID and use that.

In order to do that, we'll dive into the only piece of our resource that we haven't explored yet: operations. Operations contain the business logic of our API, and are the glue keeping policies, contracts and decorators together. In fact, all of the other components are used by operations.

What we want to do here is set the author property of new articles to the current user automatically, so we are going to edit the Create. Here's what it should look like now:

module API
  module V1
    module Article
      module Operation
        class Create < Pragma::Operation::Create
        end
      end
    end
  end
end

Not terribly exciting, right? The class is empty because all the complexity is nicely tucked into Pragma::Operation::Create, the default Pragma operation.

Here's a bit more info on the structure of operations: each operation has a success track and a failure track, and each of these tracks has multiple steps. By default, the operation's execution starts on the success tracks. The steps are run in order and their return value is checked. If a step returns false, the operation switches to the failure track and continues the execution there. Otherwise, the execution continues on the success track. (This might sound very confusing right now, but it's actually quite simple. More details will be provided at the end of this chapter.)

In our particular case, we are first going to remove the author property and setter from the contract. Then, we are going to append a step to the success track of our operation. This can be done with the step DSL method:

module API
  module V1
    module Article
      module Operation
        class Create < Pragma::Operation::Create
          step :set_author!

          def set_author!(options)
            options['model'].update!(author: options['current_user'])
          end
        end
      end
    end
  end
end

What this will do is execute the entire default Create operation and, when it's done, update the record we just created, setting the author property to the current user.

You can now try creating a new article without specifying the author, making sure to provide the appropriate authentication header:

curl 'https://our-api.example.com/api/v1/articles' \
  -X POST \
  -H 'Content-Type: application/json' \
  -H 'X-Author-Id: bad87963-d8b2-408a-876e-c84740121aed' \
  -d '{
    "title": "Hello, world!",
    "body": "This is my first article. How cool is this?"
  }'

You will notice that the new article got the author property set automatically.

However, the customization options don't just end here. In fact, the way we have implemented our new step is pretty inefficient, because it will first save the article without the author property, and then update the new article to set the author. This would fail, for instance, if we had a NOT NULL constraint on the author_id column. How can we solve this, then? We should find a way to set the author before the record is persisted to the database.

As it turns out, the step method is quite flexible, as it allows us to also specify the position of our new step. In this case, we want to set the author right before saving the record, which we can do by refactoring the Create operation like this:

module API
  module V1
    module Article
      module Operation
        class Create < Pragma::Operation::Create
          step :set_author!, before: 'persist.save'

          def set_author!(options)
            options['model'].author = options['current_user']
          end
        end
      end
    end
  end
end

The end result is exactly the same, except for the fact that now only one INSERT SQL query will be executed, instead of an INSERT followed by an UPDATE.

There is much more to operations: putting steps before other steps, replacing existing steps, destructuring options, creating, using and configuring macros, visualizing the steps... The possibilities are almost endless, and the API pretty simple. Since Pragma operations are based on Trailblazer, you should look at their documentation first, which will give you a very solid understanding of how operations work. Then, you can explore the source code of Pragma's default operations to understand what steps they execute and how you can tweak them.

The next steps

Now that you understand the basics of contracts, decorators, policies and operations, you are ready to start building APIs with Pragma. Here's what you can do to expand your knowledge:

  • Dive into the documentation and source code of Contract, Decorator, Policy and Operation. Make sure that you understand the fundamentals of how these components work together. Pragma is a huge shift from the Rails Way and can be difficult to grasp at first, but once you understand the basics your productivity will skyrocket.
  • Dive into the documentation and source code of Trailblazer. Remember that pretty much all of Pragma is built on top of Trailblazer, so understanding its principles and architectures will go a long way towards improving your understanding of Pragma as well.
  • Finish reading these guides!