Resources
“A
Model
is to theDatabase
what aResource
is to theAPI
”
Resources might look magical at first, but they are actually just a simple collection of a few common hooks.
Consider a traditional Rails controller:
Imagine if we had to implement the JSONAPI specification by hand, ensuring our endpoints supported sorting, pagination, filtering, etc. You’d start seeing something along these lines:
In other words…it’d be a gross mess, especially when dealing with inclusion of related resources or swapping datastores. But the basic pattern - starting with a scope and then decorating it based on incoming parameters - is incredibly powerful.
Instead of writing this code by hand every time, let’s move the boilerplate into a library and leave developers with only the part they care about - how to modify the scope:
This code lives in a Resource
. All we’re doing here is specifying Procs that modify the scope, leaving boilerplate to the underlying jsonapi_suite
library.
Of course, with ActiveRecord
, you’d see the same logic repeated here over
and over again. Let’s supply defaults to DRY up this code and end
with:
…but allow developers to override those defaults whenever they’d like:
The important thing is: you still have full control of the query.
This is why JSONAPI Suite can easily work with any datastore, from SQL
to MongoDB to HTTP requests. The “behind-the-scenes defaults” are stored
in an Adapter.
Supply blocks for one-off customizations, or package them up into an
Adapter
once those customizations become commonplace.
By default, JSONAPI Suite comes with an ActiveRecordAdapter
.
Scopes - a Generic Query-Building Pattern
If you look closely at the above examples, you can see our code breaks down into three key parts:
- Step 1: Start with a “base scope” - a default query object.
- Step 2: Modify that scope based on incoming parameters.
- Step 3: Actually fire the query.
This pattern applies to any ORM or datastore. Let’s try it with an HTTP client that accepts a hash of options. A generic Rails controller might look something like:
So our JSONAPI Suite equivalent would be:
Again, you can package this logic into an Adapter if you found yourself repeating the same logic over and over. Adapters DRY-up Resources.
This pattern applies to sorting, pagination, statistics and such as well - view the Reads documentation for more.
Associations
In the prior section, we noted the 3 key parts of query building. For associations, we need to answer 2 key questions:
- Question 1: Given an array of parents, what should the “base scope” be in order to query only relevant children?
- Question 2: Once we’ve resolved both the parents and the children, how do we associate these objects together?
Let’s switch back to vanilla ActiveRecord
for a second. We’ve resolved
the Post
s and need to fetch the Comment
s. Here’s how we’d answer
these questions:
Just like in our prior sections, we can see the same logic would repeat
over and over again each time we added a new relationship…with some slight tweaks based on
has_many/belongs_to
, non-standard foreign keys and such. So our
default ActiveRecord
adapter comes with macros that generate this
lower-level code for us:
You can dig deeper into the various ActiveRecord Association Macros here.
Let’s go back to HTTP calls. Imagine the CommentResource
worked just
like our HTTP-based PostResource
from the prior section. Let’s see how
those same questions would be answered:
The key lessons here:
scope
must return a “base scope” that can be further modified. This way we can apply additional “deep query” logic - maybe we want to sort these comments - and re-use the query-building code defined inCommentResource
. This allows the same logic at the/comments
endpoint to apply to the/posts?include=comments
endpoint.- If you’re not sure what the scope should be, look into the relevant
Resource
, particularly the #resolve method, to see how the query will actually be executed. If there is no #resolve method, it’s using the default of the relevantAdapter
.- Typically, you’d define an
ApplicationRecord
who specifies theAdapter
. You’ll see this pattern if you use our generators.
- Typically, you’d define an
- Adapters can
DRY-up this logic with
has_many
-style macros.
Writes
In the prior sections, we removed boilerplate and dropped down to only the important code of scope modification. The same basic premise applies to write operations as well. Rather than dealing with parsing the incoming payload and associating the graph of objects, Suite supplies hooks for just the parts you care about: actually persisting objects.
Just like reads, this logic is usually extracted into an Adapter
, but
you can always use super
to override, handle side effects, etc.
See the Writes section for more.
Generators
If you’re unsure of how a “default project” should look, use the
bin/rails g jsonapi:resource
generator:
bin/rails g jsonapi:resource Post title:string
This generator can also limit the controller actions:
bin/rails g jsonapi:resource Post title:string -a index
It’s highly encouraged you run these generators at least once, as you’ll get a bunch of helpful comments and understand baseline scenarios. You’re getting:
- A controller (e.g.
PostsController
) - A route (
/<api_namespace>/v1/posts
) - A
Resource
(PostResource
) - Integration spec boilerplate
- including Factories
- …and
Payload
s
- A whitelist of incoming parameters for writes
(
config/initializer/strong_resource.rb
)
Type bin/rails g jsonapi:resource --help for details
.
Wrapping Up
There’s more to learn about various ways Resource
s can be customized,
but that’s the basic premise: no magic, just removal of boilerplate.
Note: the same
Resource
logic can be re-used across endpoints, to support logic like “fetch thisPost
and itsComment
s that areactive
”. Whether you’re sideloading comments from the/posts
endpoint or accessing the/comments
endpoint directly, the sameResource
logic applies.
See the Resource documentation for more.