Rails REST Framework
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
Installation
Add this line to your application's Gemfile:
gem "rest_framework"
And then run:
bundle install
Quick Usage Tutorial
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
Serving the Root API Index
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
Resource Controllers
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
Routing
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
Development/Testing
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:
- Test App: http://127.0.0.1:3000
- API: http://127.0.0.1:3000/api
- Reports: http://127.0.0.1:3000/reports
Version 2
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.
New Features & Improvements
- Simpler setup — a single
include RESTFramework::Controllerreplaces the per-type*Mixinmodules. - Local-by-default configuration — assignments stay on the controller they're set on; wrap
shared settings in a
propagateblock to hand them down. Config no longer leaks silently onto every resource. - Declarative actions —
add_action/remove_actiondeclare extra routes with an explicitmember/collectionscope and per-declaration propagation, and can disable built-ins too, replacing theextra_actionsconfig hashes. - Unified routing — one
rest_route(accepting several names) replacesrest_resource/rest_resources/rest_root; a modelless controller serves the API root from itsindex_content. - Action delegation — mark an action
metadata: { delegate: true }to dispatch it to a model class method (collection) or record method (member), passing query params through as args/kwargs. - Consumer-driven association queries (opt-in via
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=Norall), both bounded by a per-association allowlist so an association never exposes more than its own endpoint would. page_total_count— skip theCOUNTquery so pagination stays fast on very large tables.
Behavior Changes
- Pagination is on by default (
PageNumberPaginator, page size 20), soindexresponses are bounded out of the box; opt out per controller withpaginator_class = nil. - Ordering and pagination read from the query string only, never the request body.
- A
find_byon a non-permitted field returns404rather than matching a virtual or serialized field. - Delegated actions wrap their result under a
returnkey, and raise on a missing or non-public target instead of silently 404-ing. - Removed
rrf_finalizeand theauto_finalize/freeze_confighooks. - Security hardening — per-element read-only stripping on bulk writes, an ordering-oracle fix,
sanitized
StatementInvalidmessages, and safer query-filter parsing.
Migrating from Older Versions
See the guide for details on each item.
- [ ] Replace the
*Mixinmodules withinclude RESTFramework::Controlleron the core API controller. - [ ] Set
self.model = ...on every resource controller (it's no longer inferred from the name). - [ ] Wrap inherited config in a
propagateblock — assignments are now local by default. - [ ] Convert
extra_actions/extra_member_actionshashes toadd_action/remove_action. - [ ] Render custom actions with
render(api: ...), replacing the olderapi_response(...)/render_api(...). - [ ] Replace
rest_resource/rest_resources/rest_rootwithrest_route.- Fold any dedicated root controller into the namespace's base controller, which now serves the
root via
index_content(the standalonerootaction andrest_rootare gone).
- Fold any dedicated root controller into the namespace's base controller, which now serves the
root via
- [ ] Rename
singleton_controller→singular. - [ ] Remove
rrf_finalizecalls and theauto_finalize/freeze_configconfig (all gone). - [ ] Expect paginated
indexresponses by default (self.paginator_class = nilrestores a bare array). - [ ] Rename config:
sub_fields→fields,native_serializer_associations_limit[_max]→association_limit[_max],native_serializer_include_associations_count→include_association_count; the?associations_limit=Nparam is gone. - [ ] Note client-visible behavior changes: delegated actions wrap their result under a
returnkey; a non-permittedfind_byreturns404;update_all/destroy_allare plural-only; ordering/pagination read from the query string only.