A framework for DRY RESTful APIs in Ruby on Rails.
The Problem: Building controllers for APIs usually involves writing a lot of redundant CRUD logic, and routing them can be obnoxious. Building and maintaining features like ordering, filtering, and pagination can be tedious.
The Solution: This framework implements browsable API responses, CRUD actions for your models, and features like ordering/filtering/pagination, so you can focus on your application logic.
Website/Guide: rails-rest-framework.com
Demo API: rails-rest-framework.com/api/demo
Source: github.com/gregschmit/rails-rest-framework
YARD Docs: rubydoc.info/gems/rest_framework
Add this line to your application’s Gemfile:
gem "rest_framework"
And then run:
bundle install
To add REST framework features to a controller, include the Controller module:
class ApiController < ApplicationController
include RESTFramework::Controller
# Assignments are local by default; wrap shared config in `propagate` so child controllers inherit
# it. Settings you want every resource to share belong here rather than on each child controller.
propagate do
self.page_size = 30
self.max_page_size = 100
end
end
Note: Configuration assignments are local by default —
self.x = valuesetsxon that controller alone and does not propagate to subclasses. To share a setting with every descendant (pagination, filter backends, serializer config, and so on), wrap the assignment in apropagateblock on a base controller.
Here is what the directory structure might look like for resource controllers:
controllers/
├─ api_controller.rb
└─ api/
├─ movies_controller.rb
└─ users_controller.rb
A controller without a model renders its index_content at its index path, which serves as the
API root. Because declared actions are local by default (they don’t propagate to subclasses), you
can serve the index — and any root-specific extra actions — straight from your API’s base
controller:
class ApiController < ApplicationController
include RESTFramework::Controller
add_action(:test, :get)
# Rendered at the `/api` root. Defaults to the controller's `description`.
def index_content
{
message: "Welcome to the API.",
how_to_authenticate: <<~END.lines.map(&:strip).join(" "),
You can use this API with your normal login session. Otherwise, you can insert your API key
into a Bearer Authorization header, or into the URL parameters with the name `api_key`.
END
}
end
def test
render(api: {message: "Hello, world!"})
end
end
Other API controllers can be associated to a resource/model by setting model, e.g.
self.model = Movie.
class Api::MoviesController < ApiController
self.model = Movie # Automatically routes the standard CRUD actions for this controller.
self.bulk = true # Enables bulk create/update/destroy actions for this controller.
self.fields = [:id, :name, :release_date, :enabled]
add_action(:first, :get, type: :member)
def first
# Always use bang methods, since the framework will rescue `RecordNotFound` and return a
# sensible error response.
render(api: self.get_records.first!)
end
def get_recordset
return Movie.where(enabled: true)
end
end
When fields is nil, then it will default to all columns. The fields attribute can also be a hash
to include or exclude fields rather than defining them manually:
class Api::UsersController < ApiController
# Include a method `popularity` and exclude the `impersonation_token` column.
self.fields = {include: [:popularity], exclude: [:impersonation_token]}
# You can even disable some of the builtin actions. For example, this effectively makes the
# resource read-only:
remove_actions(:create, :update, :destroy, :update_all, :destroy_all)
end
Use rest_route to route any controller. It wraps Rails’ resource / resources routers, picking
resources for a plural model controller and resource otherwise, and automatically routes the
controller’s built-in and extra actions. A controller with a model gets the full CRUD set; a
modelless controller is routed at its root (its index, which renders index_content).
Rails.application.routes.draw do
rest_route :api # `ApiController` serves the `/api` root.
namespace :api do
rest_route :movies
rest_route :users
end
end
After you clone the repository, cd’ing into the directory should create a new gemset if you are
using RVM. Then run bin/setup to install the appropriate gems and set things up.
The top-level bin/rails proxies all Rails commands to the test project, so you can operate it via
the usual commands (e.g., rails test, rails console). For development, use bin/dev to run the
web server and the job queue, which serves the test app and coverage/brakeman reports:
Version 2 is a substantial overhaul. The highlights below cover the major additions and behavior changes; the migration checklist that follows walks through updating an existing app.
include RESTFramework::Controller replaces the per-type *Mixin
modules.propagate block to hand them down. Config no longer leaks silently onto
every resource.add_action / remove_action declare extra routes with an explicit
member / collection scope and per-declaration propagation, and can disable built-ins too,
replacing the extra_actions config hashes.rest_route (accepting several names) replaces rest_resource /
rest_resources / rest_root; a modelless controller serves the API root from its
index_content.metadata: { delegate: true } to dispatch it to a model
class method (collection) or record method (member), passing query params through as args/kwargs.enable_association_queries) — clients can
request extra fields for a serialized association (?associations.<name>.fields=a,b,c) and raise
its per-request record limit (?associations.<name>.limit=N or all), both bounded by a
per-association allowlist so an association never exposes more than its own endpoint would.page_total_count — skip the COUNT query so pagination stays fast on very large tables.PageNumberPaginator, page size 20), so index responses are
bounded out of the box; opt out per controller with paginator_class = nil.find_by on a non-permitted field returns 404 rather than matching a virtual or
serialized field.return key, and raise on a missing or non-public
target instead of silently 404-ing.rrf_finalize and the auto_finalize / freeze_config hooks.find_by, filtering, ordering, and search are scoped to serialized,
non-write_only fields (so hidden/secret columns can’t be used as lookup/enumeration keys), plus
per-element read-only stripping on bulk writes, an ordering-oracle fix, sanitized
StatementInvalid messages, and safer query-filter parsing.See the guide for details on each item.
*Mixin modules with include RESTFramework::Controller on the core API
controller.self.model = ... on every resource controller (it’s no longer inferred from the name).propagate block — assignments are now local by default.extra_actions / extra_member_actions hashes to add_action / remove_action.render(api: ...), replacing the older api_response(...) /
render_api(...).rest_resource / rest_resources / rest_root with rest_route.index_content (the standalone root action and rest_root are gone).singleton_controller → singular.rrf_finalize calls and the auto_finalize / freeze_config config (all gone).index responses by default (self.paginator_class = nil restores a bare
array).sub_fields → fields, native_serializer_associations_limit[_max] →
association_limit[_max], native_serializer_include_associations_count →
include_association_count; the ?associations_limit=N param is gone.return
key; a non-permitted find_by returns 404; update_all / destroy_all are plural-only;
ordering/pagination read from the query string only.