Terrain
Opinionated toolkit for building CRUD APIs with Rails
Install
Add Terrain to your Gemfile:
gem 'terrain'
Usage
- Error handling
- Resources
- Authorization
- Serialization
- Querying
- Filtering
- Ordering
- Pagination
- Relationships
- CRUD operations
- Config
Error handling
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
Resources
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
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
).
Serialization
Querying
Records of a given resource are queried by requesting the index
action.
Filtering
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
Ordering
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'
Pagination
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).
Relationships
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.
CRUD operations
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
Config
Terrain.configure do |config|
# Maximum number of records returned
config.max_records = Float::INFINITY
end