Opinionated toolkit for building CRUD APIs with Rails
Add Terrain to your Gemfile:
gem 'terrain'
class ExampleController < ApplicationController
include Terrain::Errors
end
Rescues the following errors:
ActiveRecord::AssociationNotFoundError
(400)Pundit::NotAuthorizedError
(403)ActiveRecord::RecordNotFound
(404)ActionController::RoutingError
(404)ActiveRecord::RecordInvalid
(422)
JSON responses are of the form:
{
"error": {
"key": "type_of_error",
"message": "Localized error message",
"details": "Optional details"
}
}
To rescue a custom error with a similar response:
class ExampleController < ApplicationController
include Terrain::Errors
rescue_from MyError, with: :my_error
private
def my_error
error_response(:type_of_error, 500, { some: :details })
end
end
Suppose you have an Example
model with foo
, bar
, and baz
columns.
class ExampleController < ApplicationController
include Terrain::Resource
resource Example, permit: [:foo, :bar, :baz]
end
This sets up the typical resourceful Rails controller actions. Note that you'll still need to setup corresponding routes.
Authorization is handled by Pundit. If the policy class for a given resource exists, each controller action calls the policy before proceeding with the operation. Authorization expects a current_user
controller method to exist (otherwise nil
is used as the pundit_user
).
Records of a given resource are queried by requesting the index
action.
Queries are scoped to the results returned from the resource_scope
method. By default this returns all records, however, you can override it to further filter the results (i.e. based on query params, nested route params, etc.):
class ExampleController < ApplicationController
include Terrain::Resource
resource Example, permit: [:foo, :bar, :baz]
private
def resource_scope
scope = super
scope = scope.where(foo: params[:foo]) if params[:foo].present?
scope
end
end
You can pass an order
param to reorder the response records. Specify a comma-separated list of fields and prefix the field with a -
for descending order:
# corresponds to Example.order('foo', 'bar desc')
get :index, order: 'foo,-bar'
To request a range of records, specify the range in an HTTP header:
# Request the first 10 records
get :index, {}, { 'Range' => '0-9' }
All responses include a Content-Range
header that specifies the exact range returned as well as a total count of records. i.e.
Content-Range: 0-9/100
You can also pass open ended ranges such as 10-
(i.e. skip the first 10 records).
No model relationships are serialized in the response by default. To specify the set of relationships to be embedded in the response, pass a comma-separated list of relationships in the include
param.
As an example, suppose we're querying for posts which each have many tags and belong to an author. We could embed those relationships with the following include
param:
get :index, include: 'author,tags'
Suppose now that the author also has a profile relationship. We could include the author, author profile and tags by passing:
get :index, include: 'author.profile,tags'
Included relationships are automatically preloaded via the ActiveRecord includes
method. The include
param is also supported in show
actions.
You may need an action to perform additional steps beyond simple persistence. There are methods for performing CRUD operations that can be overridden (shown below with their default implementation):
class ExampleController < ApplicationController
include Terrain::Resource
resource Example, permit: [:foo, :bar, :baz]
private
def create_record
resource.create!(permitted_params)
end
def update_record(record)
record.update_attributes!(permitted_params)
record
end
def destroy_record(record)
record.delete
end
end
Terrain.configure do |config|
# Maximum number of records returned
config.max_records = Float::INFINITY
end