Resources

“A Model is to the Database what a Resource is to the API

Resources might look magical at first, but they are actually just a simple collection of a few common hooks.

Consider a traditional Rails controller:

def index
  posts = Post.all
  render json: posts
end

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:

# No query has fired yet, this is a blank ActiveRecord scope
posts = Post.all

if title = params[:filter].try(:title)
  # Alter the scope if we're filtering
  posts = posts.where(title: title)
end

# ... etc ...

if sort = params[:sort]
  # Alter the scope if we're sorting
  sort_dir = :asc
  if sort.starts_with?('-')
    sort_dir = :desc
  end
  sort_att = sort.split('-')[1]
  posts = posts.order(sort_att => sort_dir)
end

# ... etc ...

render json: posts.to_a # Finally!

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:

allow_filter :title do |scope, value|
  scope.where(title: value)
end

sort do |att, dir|
  scope.order(att => dir)
end

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:

# Whitelist the filter
allow_filter :title

…but allow developers to override those defaults whenever they’d like:

allow_filter :title do |scope, value|
  scope.where(["title LIKE ?", "#{value}%"])
end

sort do |attribute, direction|
  # ... your custom sort logic ...
end

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:

def index
  # Step 1: Our "base scope"
  scope = {}

  # Step 2: Modify that scope based on the request
  if title = params[:filter].try(:[], :title)
    scope[:title] = title
  end

  # Step 3: actually fire the request + build some models
  # Post here is a PORO (plain old ruby object)
  hashes = HTTP.get('/posts', scope)
  posts = hashes.map { |attr| Post.new(attrs) }

  # render
  render json: posts
end

So our JSONAPI Suite equivalent would be:

# Step 1: Define the base scope in the controller
def index
  base_scope = {}
  # Pass the base scope to the resource, which will
  # build + fire the query.
  #
  # Then, render the results.
  render_jsonapi(base_scope)
end
# app/resources/post_resource.rb
#
# Step 2: Modify the scope in the Resource
allow_filter :title do |scope, value|
  scope[:title] = value
end

# Step 3: Actually fire the query
# This method must return an array of Model instances
def resolve(scope)
  hashes = HTTP.get('/posts', scope)
  hashes.map { |attr| Post.new(attrs) }
end

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 Posts and need to fetch the Comments. Here’s how we’d answer these questions:

allow_sideload :comments, resource: CommentResource do
  # Question 1: What's a "base scope" that will return only
  # relevant comments?
  scope do |posts|
    Comment.where(post_id: posts.map(&:id))
  end

  # Question 2: How do we assign these objects together?
  assign do |posts, comments|
    posts.each do |post|
      post.comments = comments.select { |c| c.post_id == post.id }
    end
  end
end

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:

has_many :comments,
  resource: CommentResource,
  scope: -> { Comment.all },
  foreign_key: :post_id

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:

# Step 1: What's a base scope that will return only
# relevant comments?
#
# In the case of our HTTP client, the "base scope" is
# nothing more than a ruby hash.
#
# Our final query would end up something like:
#
# HTTP.get('/comments', { post_id: [1,2,3] })
scope do |posts|
  { post_id: posts.map(&:id) }
end

# Step 2: How do we assign these objects together?
# This code is unchanged from the prior example
assign do |posts, comments|
  posts.each do |post|
    post.comments = comments.select { |c| c.post_id == post.id }
  end
end

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 in CommentResource. 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 relevant Adapter.
    • Typically, you’d define an ApplicationRecord who specifies the Adapter. You’ll see this pattern if you use our generators.
  • 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.

def create(attributes)
  post = Post.new(attributes)
  post.save
  post
end

def update(attributes)
  post = Post.find(attributes.delete(:id))
  post.update_attributes(attributes)
  post
end

# ... etc ...

Just like reads, this logic is usually extracted into an Adapter, but you can always use super to override, handle side effects, etc.

def create(attributes)
  model = super
  Rails.logger.info "#{model.class} created with id #{model.id}!"
  model
end

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 Payloads
  • 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 Resources 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 this Post and its Comments that are active”. Whether you’re sideloading comments from the /posts endpoint or accessing the /comments endpoint directly, the same Resource logic applies.

See the Resource documentation for more.