-
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.
- Users can be writers or admins.
- 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.
- Admins can see all articles.
- Admins can edit and delete all 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, writers can edit/delete
their own articles only, and admins have full control over all articles. We're going to assume your
app has a User
model with a role
attribute which is either writer
or admin
.
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
contract are empty, which means they will just inherit the behavior of
Base
.