0.01
A long-lived project that still receives updates
A framework for DRY RESTful APIs in Ruby on Rails.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Runtime

>= 4.0
 Project Readme

Rails REST Framework

Gem Version Pipeline Coverage

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 defaultself.x = value sets x on 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 a propagate block 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 Base 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_resource to route a controller (rest_resources routes several that share options). They wrap Rails' resource / resources routers, picking resources for a plural model controller and resource otherwise — so plurality follows the controller's config, not the helper name. Built-in and extra actions are routed automatically: a controller with a model gets the full CRUD set; a modelless controller is routed at its root (its index, which renders index_content). A block nests resources, and a nested recordset is auto-scoped to its parent (/movies/:movie_id/genresMovie.find(params[:movie_id]).genres).

Rails.application.routes.draw do
  rest_resource :api  # `ApiController` serves the `/api` root.

  namespace :api do
    rest_resources :movies, :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:

Releasing

The gem version is in lib/rest_framework/version.rb. Cutting a release means bumping that constant, tagging, and pushing; the pipeline builds the gem from the constant and pushes it to RubyGems when it sees the tag.

Use bin/release with the exact version. It bumps the version constant, refreshes Gemfile.lock, folds both into a single commit, and creates the annotated tag v<version> (via the gem-release gem). The lock refresh matters because rest_framework is a path gem: if the tagged commit's lock doesn't match the bumped gemspec, CI's frozen bundle install fails with "the gemspecs for path gems changed". Run it from the master branch with a clean working tree:

bin/release 2.0.0.rc1   # cut a release candidate

Review, then push to publish:

git push origin master --follow-tags

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::Controller replaces the per-type *Mixin modules.
  • Local-by-default configuration — assignments stay on the controller they're set on; wrap shared settings in a propagate block to hand them down. Config no longer leaks silently onto every resource.
  • Declarative actionsadd_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.
  • Simpler routingrest_resource routes one controller and rest_resources routes several (plurality of the routes follows the controller's config, not the helper name); rest_root and rest_route are gone, and nested resources are auto-scoped to their parent.
  • 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=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.

Behavior Changes

  • Pagination is on by default (PageNumberPaginator, page size 20), so index responses are bounded out of the box; opt out per controller with paginator_class = nil.
  • Ordering and pagination read from the query string only, never the request body.
  • A find_by on a non-permitted field returns 404 rather than matching a virtual or serialized field.
  • Delegated actions wrap their result under a return key, and raise on a missing or non-public target instead of silently 404-ing.
  • Removed rrf_finalize and the auto_finalize / freeze_config hooks.
  • Security hardeningfind_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.

Migrating from Older Versions

See the guide for details on each item.

  • Replace the *Mixin modules with include RESTFramework::Controller on the core API controller.
  • Set self.model = ... on every resource controller (it's no longer inferred from the name).
  • Wrap inherited config in a propagate block — assignments are now local by default.
  • Convert extra_actions / extra_member_actions hashes to add_action / remove_action.
  • Render custom actions with render(api: ...), replacing the older api_response(...) / render_api(...).
  • Replace rest_root and rest_route with rest_resource (single controller) or rest_resources (several) — plurality of the routes now comes from controller config.
  • Fold any dedicated root controller into the namespace's base controller, which now serves the root via index_content (the standalone root action and rest_root are gone).
  • Rename singleton_controllersingular.
  • Remove rrf_finalize calls and the auto_finalize / freeze_config config (all gone).
  • Expect paginated index responses by default (self.paginator_class = nil restores a bare array).
  • Rename config: sub_fieldsfields, native_serializer_associations_limit[_max]association_limit[_max], native_serializer_include_associations_countinclude_association_count, native_serializer_{only,except,include,exclude}_query_param{only,except,include,exclude}_query_param; the ?associations_limit=N param is gone.
  • Move field_config into fields: it's now the config: key of the fields hash (self.fields = { only: [...], config: { email: { label: "Email Address" } } }). A field named in config: is implicitly part of the set. An association's fields: takes the same spec form (an array, or an only:/include:/exclude:/config: hash), replacing the old nested field_config: key.
  • Replace the native_serializer_config / native_serializer_singular_config / native_serializer_plural_config attributes with a custom serializer_class — a NativeSerializer subclass carrying config / singular_config / plural_config.
  • Note client-visible behavior changes: delegated actions wrap their result under a return key; a non-permitted find_by returns 404; update_all / destroy_all are plural-only; ordering/pagination read from the query string only.