-
Notifications
You must be signed in to change notification settings - Fork 3
Your first Pragma API
Now that you understand the concepts behind Pragma, it's time to start building something!
In this guide, we are going to implement a basic API with Pragma for managing articles in a blog. Here are our business requirements:
- An article has an author, title, body and a "published" boolean flag.
- The blog has multiple users.
- All users can view all published articles.
- Writers can see articles they have authored, even if not published.
- Writers can only edit and delete their own articles.
We will first implement the API resource with plain Pragma and then see how we can expose it in a Rails application with Pragma::Rails. For the purpose of this guide, we will assume you already have a working Rails application. If you don't, you can use pragma-rails-starter.
The first step is installing Pragma in your application. Luckily, this is pretty simple! Just add
the following to your Gemfile
:
gem 'pragma'
This will install the main pragma gem, which wraps the core components and provides default CRUD operations.
The gem doesn't require any configuration, so you're good to go!
Now, we are going to create the directory structure for our API resource.
In a Rails application, Pragma resources are usually stored in app/resources
, but you can put them
wherever you want as long as you can access them from a controller.
In general, Pragma doesn't care about the directory structure of your code, but it is very sensitive to the module/class hierarchy: the default CRUD operations assume a very specific set of module/class names and will not work if you deviate from the recommended structure (unless you reconfigure them, of course).
For this guide, let's go with the default directory and file structure, which is the following:
app/resources/api/v1/article
├── contract
│ ├── base.rb
│ ├── create.rb
│ └── update.rb
├── decorator
│ ├── collection.rb
│ └── instance.rb
├── operation
│ ├── create.rb
│ ├── destroy.rb
│ ├── index.rb
│ ├── show.rb
│ └── update.rb
└── policy.rb
As you can see, all resources are versioned, with the first version of your API having the API::V1
namespace. How you manage versioning is up to you, but usually you should only bump your version
number when you introduce breaking changes to a published and adopted API. There are alternative
approaches (see Pragma::Migration), but for now
we're going to stick to the default scheme.
For the time being you can leave all files blank, we're going to fill them later.
The first component we're going to configure for the Article
resource is the policy. As mentioned
in the introduction, policies authorize operations that users want to perform on your API resources.
As far as authorization goes, we said that all users can see all articles, and that they can edit/delete their own articles only.
Let's open our policy and enter the following (no worries, we're going to explain all of this):
# app/resources/api/v1/article/policy.rb
module API
module V1
module Article
class Policy < Pragma::Policy::Base
class Scope < Pragma::Policy::Scope
def resolve
filtered_scope = scope.where(published: true)
filtered_scope = filtered_scope.or(scope.where(author: user)) if user
filtered_scope
end
end
def show?
record.published? || record.author == user
end
def create?
true
end
def update?
record.author == user
end
def destroy?
record.author == user
end
end
end
end
end
As you can see, the first thing we do is define an API::V1::Article::Policy::Scope
class that
implements a single resolve
method. The scope is a special class in our policy whose job is to
filter the collection of records returned by the Index operation. When a user requests the list of
articles, Pragma will first load all the articles in the list, then pass that collection to the
scope of your policy to retrieve only the articles accessible by the current user. Our scope in this
case only returns articles that are published or belong to the current user, if the user is
authenticated.
After the scope, we are defining the policies for authorizing the Show, Create, Update and Destroy operations. These should be pretty straightforward, so we won't go into the details.
You might be wondering why the create?
policy simply returns true
without checking whether the
user is authenticated. This is because policies are only concerned with authorization, not
authentication. In other words, you should never have to return false
early in a policy when
user
is nil
, since that's an operation's concern.
If this looks very similar to Pundit, it's because we used it as the inspiration for the syntax and functionality of Pragma::Policy. In fact, until not long ago, Pragma::Policy used to be just an extension of Pundit!
Now that we have our policy, let's take care of our decorators.
As you can see from the file structure, each resource has two decorators by default: a collection decorator and an instance decorator. As you can imagine, the instance decorator converts a single article to JSON, while the collection decorator works on collections. This distinction is necessary because collections will usually include additional metadata about pagination and so on.
This is what our instance decorator for the Article
resource might look like:
# app/resources/api/v1/article/decorator/instance.rb
module API
module V1
module Article
module Decorator
class Instance < Pragma::Decorator::Base
include Pragma::Decorator::Type
property :id
property :title
property :body
property :published
end
end
end
end
end
This should be pretty simple to understand: the Pragma::Decorator::Type
module is a small mixin
that will add a type
property to your resource's JSON representation. This property contains a
machine-readable name for the resource type, so that clients can easily understand what kind of
resource they're dealing with. In this case, type
will be article
.
The property
definitions simply tell the decorator that the id
, title
, body
and published
property should all be exposed to the API clients.
Now, let's define our collection decorator:
# app/resources/api/v1/article/decorator/collection.rb
module API
module V1
module Article
module Decorator
class Collection < Pragma::Decorator::Base
include Pragma::Decorator::Type
include Pragma::Decorator::Collection
include Pragma::Decorator::Pagination
decorate_with Instance
end
end
end
end
end
There's a bit more stuff going on here.
First of all, we're requiring the same Type
module as in the instance decorator, but this time
type
will be list
(a language-agnostic version of array
).
Then, we're also including Collection
and Pagination
, which will, respectively, wrap the
collection's entries in a data
property and add pagination metadata at the root level.
Finally, we're telling the decorator that our instance decorator is called Instance
and should be
used to decorate our collection's entries.
Let's move on to the contracts now. As you see, there are three contracts in your usual resource:
the Create
and Update
contracts are used in the respective operations and they both inherit from
the Base
contract which contains shared properties and validation logic.
Our Article
resource is pretty simple, so we're going to define all three contracts together and
then explain them briefly:
# app/resources/api/v1/article/contract/base.rb
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)
validation do
required(:title).filled
required(:body).filled
end
end
end
end
end
end
# app/resources/api/v1/article/contract/create.rb
module API
module V1
module Article
module Contract
class Create < Base
end
end
end
end
end
# app/resources/api/v1/article/contract/update.rb
module API
module V1
module Article
module Contract
class Update < Base
end
end
end
end
end
The Base
contract only defines three properties: title
, body
and published
. The type
option defines the type of the property for coercion. You can see that title
and body
are
strings while published
is a boolean. There are many different types, you can see them all in the
Dry::Types documentation (Pragma::Contract uses
Dry::Types under the hood).
The validation
block defines the validation rules that will be applied to the properties. In this
case, we're expecting title
and body
to be filled. This syntax comes from Dry::Validation
and is very powerful, allowing you to define complex validation logic in a highly maintainable way.
The Create
and Update
contracts are empty, which means they will just inherit the behavior of
Base
.
The last step in creating our resource is defining some operations for it. Luckily, the pragma gem comes with default CRUD operations that we can simply inherit from to get all the functionality we need. In order to implement the Article API endpoints, here's all we need to do:
# app/resources/api/v1/article/operation/index.rb
module API
module V1
module Article
module Operation
class Index < Pragma::Operation::Index
end
end
end
end
end
# app/resources/api/v1/article/operation/show.rb
module API
module V1
module Article
module Operation
class Show < Pragma::Operation::Show
end
end
end
end
end
# app/resources/api/v1/article/operation/create.rb
module API
module V1
module Article
module Operation
class Create < Pragma::Operation::Create
end
end
end
end
end
# app/resources/api/v1/article/operation/update.rb
module API
module V1
module Article
module Operation
class Update < Pragma::Operation::Update
end
end
end
end
end
# app/resources/api/v1/article/operation/destroy.rb
module API
module V1
module Article
module Operation
class Destroy < Pragma::Operation::Destroy
end
end
end
end
end
These operations are very powerful: they can be customized to suit your needs and they can be extended with your own business/presentation logic. To find out more, we encourage you to read their source code and the rest of the guides in this wiki.
But wait, aren't we forgetting something? Our Article
model has an author
property which must be
filled with the user creating the aticle in order for authorization to work properly. In order to do
that, we can add a new step to the Create
operation:
# app/resources/api/v1/article/operation/create.rb
module API
module V1
module Article
module Operation
class Create < Pragma::Operation::Create
step :set_author!, after: 'contract.default.validate'
def set_author!(model:, current_user:, **)
model.author = current_user
end
end
end
end
end
end
There is a lot going on here, but basically what we're doing is defining a new step for the
operation that will run after the contract validates successfully (and before the record is saved).
This step will set the model's author
property to the value of current_user
.
The Operation API is actually very powerful and a big shift in how developers usually reason about business logic, so you're encouraged to read more about it in the Pragma::Operation documentation and the rest of these guides.
If you're wondering how current_user
is passed to the operation, just read below!
Our resource is now ready, but how do we expose it in our Rails app and allow people to make actual HTTP requests?
In order to do that, you need to install the pragma-rails
gem, which can replace pragma
in your
Gemfile
:
# before
gem 'pragma'
# after
gem 'pragma-rails'
Once the gem is installed, all you need to do is create a controller for the API resource:
# app/controllers/api/v1/articles_controller.rb
module API
module V1
class ArticlesController < ApplicationController
include Pragma::Rails::ResourceController
before_action :authenticate_user, except: %i(index show)
private
def authenticate_user
unless current_user
head :unauthorized
return false
end
end
def current_user
User.find_by(auth_token: request.headers['X-Auth-Token'])
end
end
end
end
The Pragma::Rails::ResourceController
module maps the controller's actions to the respective
Pragma operations, taking care of forwarding any input from the HTTP requests and responding with
the result of the operation.
Note that we're also defining some very simple (and insecure!) authentication logic for the sake of
this example. Pragma assumes you have a current_user
method in your controller that returns a user
which will be forward to the operation, so we need to define this method in our controller. You can
read more about this in the Pragma::Rails documentation.
Finally, you need to define the routes for your new controller:
# config/routes.rb
Rails.application.routes.draw do
# ...
namespace :api do
namespace :v1 do
resources :articles, only: %i(index show create update destroy)
end
end
end
That's it! You can now boot your Rails server and start playing with your new API!
Congratulations! Now you have your first fully working API built with Pragma. There's a lot more ground to cover, but before continuing with the guides it's strongly recommended that you read the documentation of the individual components we have used in this guide, to get a sense of how powerful they are and how it all fits together: