This is the core of the REST Framework. Generally speaking, projects already have an existing controller inheritance hierarchy, so we want developers to be able to maintain that project structure while leveraging the power of the REST Framework. Also, different controllers which inherit from the same parent often need different REST Framework functionality and behavior. For these reasons, REST Framework provides the controller functionality as modules that you mix into your controllers, following the pattern of “composition over inheritance”.
Here are the 4 controller mixins, one of which should always be included into any API controller. There are accessory mixins listed below some of the controller mixins, which are included by default to provide specific CRUD functionality.
┌─ BaseControllerMixin
│ └─ BaseModelControllerMixin (includes BaseControllerMixin)
├─ ModelControllerMixin (includes BaseModelControllerMixin)
│ ├─ ListModelMixin
│ ├─ ShowModelMixin
│ ├─ CreateModelMixin
│ ├─ UpdateModelMixin
│ └─ DestroyModelMixin
├─ ReadOnlyModelControllerMixin (includes BaseModelControllerMixin)
│ ├─ ListModelMixin
│ └─ ShowModelMixin
└─ BulkModelControllerMixin (includes ModelControllerMixin)
├─ BulkCreateModelMixin
├─ BulkUpdateModelMixin
└─ BulkDestroyModelMixin
Again, all API controllers should include at least one of these 4 top-level controller mixins, and can include any of the supplementary mixins to add additional functionality. For example, if you want to permit create but not update or destroy, then you could do this:
class Api::MoviesController < ApiController
include RESTFramework::BaseModelControllerMixin
include RESTFramework::CreateModelMixin
end
To transform a controller into the simplest possible RESTful controller, you can include
BaseControllerMixin
, which provides a simple root
action so it can be used at the API root.
class ApiController < ApplicationController
include RESTFramework::BaseControllerMixin
end
A fundamental feature that REST Framework provides is the ability to render a browsable API. This allows developers to discover and interact with the API’s functionality, while also providing faster and more lightweight JSON/XML formats for consumption by the systems these developers create.
The api_response
method is how this is accomplished. The first argument is the data to be
rendered (often an array or hash), and keyword arguments can be provided to customize the response
(e.g., setting the HTTP status code). Using this method instead of the classic Rails helpers, such
as render
, will automatically provide the browsable API as well as JSON/XML rendering.
Here is a simple example of rendering a hash with a message
key:
class ApiController < ApplicationController
include RESTFramework::BaseControllerMixin
self.extra_actions = {test: :get}
def test
api_response({message: "Test successful!"})
end
end
The extra_actions
property defines extra actions on the controller to be routed. It is a hash of
endpoint -> method(s)
(where method(s)
can be a method symbol or an array of method symbols).
class ApiController < ApplicationController
include RESTFramework::BaseControllerMixin
self.extra_actions = {test: :get}
def test
api_response({message: "Test successful!"})
end
end
Or with multiple methods:
class ApiController < ApplicationController
include RESTFramework::BaseControllerMixin
self.extra_actions = {test: [:get, :post]}
def test
api_response({message: "Test successful!"})
end
end
If your action conflicts with a builtin method, then you can also override the path:
class ApiController < ApplicationController
include RESTFramework::BaseControllerMixin
# This will route `test_action` to `/test`, in case there is already a `test` method that cannot
# be overridden.
self.extra_actions = {test_action: {path: :test, methods: :get}}
def test_action
api_response({message: "Test successful!"})
end
end
ModelControllerMixin
assists with providing the standard model CRUD (create, read, update,
destroy) for your controller. This is the most commonly used mixin since it provides default
behavior for models which matches Rails’ resourceful routing.
class Api::MoviesController < ApiController
include RESTFramework::ModelControllerMixin
end
By default, all columns and associations are included in the controller’s fields
, which can be
helpful when developing an administrative API. For most APIs, however, fields
should be explicitly
defined. See Specifying the Fields for more information.
The model
property allows you to define the model if it is not derivable from the controller name.
class Api::CoolMoviesController < ApiController
include RESTFramework::ModelControllerMixin
self.model = Movie
end
The recordset
property allows you to define the set of records this API should be limited to. If
you need to change the recordset based on properties of the request, then you can override the
get_recordset
method.
class Api::CoolMoviesController < ApiController
include RESTFramework::ModelControllerMixin
self.recordset = Movie.where(cool: true).order({id: :asc})
end
If you override get_recordset
, you should ensure you also set the model
property if it’s not
derivable from the controller name.
class Api::CoolMoviesController < ApiController
include RESTFramework::ModelControllerMixin
self.model = Movie
def get_recordset
return Movie.where(cool: true).order({id: :asc})
end
end
While extra_actions
(and the synonym extra_collection_actions
) allows you to define additional
actions on the controller, extra_member_actions
allows you to define additional member actions on
the controller, which require a record ID to be provided as a path param.
class Api::MoviesController < ApiController
include RESTFramework::ModelControllerMixin
self.extra_member_actions = {disable: :patch}
def disable
# REST Framework will rescue ActiveRecord::RecordNotFound.
@record = self.get_record
# REST Framework will rescue ActiveRecord::RecordInvalid or ActiveRecord::RecordNotSaved.
@record.update!(enabled: false)
return api_response(@record)
end
end
The fields
property defines the fields available for things like serialization and allowed
parameters in body or query params. If fields
is not set, then it will default to all columns and
associations, which is helpful when developing an administrative API. For most APIs, however,
fields
should be explicitly defined.
While you can also define per-request fields by overriding get_fields
, you should also define a
set of fields on the controller which is used for things like the OPTIONS
metadata.
class Api::MoviesController < ApiController
include RESTFramework::ModelControllerMixin
self.fields = [:id, :name]
end
You can also mutate the default set of fields by removing existing columns/associations, and even adding methods. The framework will automatically detect if the given field is a column, association, or method, and will handle it appropriately.
class Api::MoviesController < ApiController
include RESTFramework::ModelControllerMixin
# This will include all columns, all associations except `owners`, and the `is_featured` method.
self.fields = {
exclude: [:owners],
include: [:is_featured],
}
end
For most cases, the default serializer configuration is sufficient, and can be modified by adjusting
the fields
property on the controller. However, there are cases where you may want to define a
serializer configuation, such as when you want to customize nested associations, or if you want to
remove certain fields (like methods) when serializing multiple records.
The property native_serializer_config
defines the serializer configuration if you are using the
default serializer. You can also specify serializers for singular/plural data.
class Api::MoviesController < ApiController
include RESTFramework::ModelControllerMixin
self.native_serializer_config = {
only: [:id, :name],
methods: [:active, :some_expensive_computed_property],
include: {cast_members: { only: [:id, :name], methods: [:net_worth] }},
}
# Or you could configure a default and a plural serializer:
self.native_serializer_config = {
only: [:id, :name],
methods: [:active, :some_expensive_computed_property],
include: {cast_members: { only: [:id, :name], methods: [:net_worth] }},
}
self.native_serializer_plural_config = {
only: [:id, :name],
methods: [:active],
include: {cast_members: { only: [:id, :name], methods: [:net_worth] }},
}
# Or you could configure a default and a singular serializer:
self.native_serializer_config = {
only: [:id, :name],
methods: [:active],
include: {cast_members: { only: [:id, :name], methods: [:net_worth] }},
}
self.native_serializer_singular_config = {
only: [:id, :name],
methods: [:active, :some_expensive_computed_property],
include: {cast_members: { only: [:id, :name], methods: [:net_worth] }},
}
end
The allowed_parameters
property defines the allowed parameters for create/update actions. The
framework uses strong parameters to execute this filtering.
class Api::MoviesController < ApiController
include RESTFramework::ModelControllerMixin
self.allowed_parameters = [:name]
end
If you want different allowed parameters for create/update actions, or if you need stronger control
over what request body parameters get passed to create/update, then you can also override the
get_create_params
or get_update_params
methods.
The create_from_recordset
attribute (true
by default) is a boolean to control the behavior in
the create
action. If it is disabled, records will not be created from the filtered recordset, but
rather will be created directly from the model interface.
For example, if this is your controller:
class Api::CoolMoviesController < ApiController
include RESTFramework::ModelControllerMixin
# Notice that `cool` is read-only with the following two configurations:
self.fields = [:id, :name]
self.native_serializer_config = {only: [:id, :name, :cool]}
# New records created from this controller will have `cool` set to `true`.
def get_recordset
return Movie.where(cool: true)
end
end
Then if you hit the create
action with the payload {name: "Superman"}
, it will also set cool
to true
on the new record, because that property is inherited from the recordset.
ReadOnlyModelControllerMixin
only enables list/show actions.
class Api::ReadOnlyMoviesController < ApiController
include RESTFramework::ReadOnlyModelControllerMixin
self.model = Movie
end