API Contributor's Guide
The API has been growing quickly thanks to our many contributors. In an effort to help new contributors get up to speed quickly, it was about time for a blog post to explain the process of adding in a new collection and subcollection, as well as provide some examples for commonly asked questions.
The api.yml is where a lot of the magic happens. The best way to explain it is through an example, so let’s walk through adding in a new collection and subcollection.
We’ll keep the example generic through the creation of a Coffee
API. Because who doesn’t need more in their life?
Defining a new collection with basic CRUD
The new Coffee
collection will need to be defined under collections in the api.yml
. Note that all of the collections are in alphabetical order.
The collection will need to return details about the coffee, update them if they need some more sugar, create new ones (Pumpkin, anyone?), and delete any we’re not a fan of - basic CRUD.
Here is a glimpse into what adding the new collection with basic CRUD will look like:
:collections:
:coffees:
:description: Coffee
:options:
- :collection
:verbs: *gpd
:klass: Coffee
:collection_actions:
:get:
- :name: read
:identifier: coffee_show_list
:post:
- :name: create
:identifier: coffee_new
- :name: edit
:identifier: coffee_edit
- :name: delete
:identifier: coffee_delete
:resource_actions:
:get:
- :name: read
:identifier: coffee_show
:post:
- :name: edit
:identifier: coffee_edit
- :name: delete
:identifier: coffee_delete
:delete:
- :name: delete
:identifier: coffee_delete
Below is a breakdown of what some of the above elements mean:
-
:coffees:
- Defines the collection or subcollection portion of the URI in conjunction with the name and supported verbs (in our case,
GET api/coffee
,POST /api/coffee
, etc..) - Tells the API that the controller will be
CoffeesController
- Defines the collection or subcollection portion of the URI in conjunction with the name and supported verbs (in our case,
-
:options:
- Gives more details about the collection and whether it will act as a collection, subcollection
-
:verbs:
- Specifies the verb set that can be used on the collection
-
:klass:
- Specifies the base class name of the resources under this collection
-
:name:
- Name of the action to be performed
-
:identifier:
- Name of the corresponding product feature, which is used to control permissions to resources
-
:collection_actions: vs :resource_actions:
- Collection actions correspond to those against the collection
/api/coffee
and resource actions are those corresponding to a single resource/api/coffee/:c_id
- Collection actions correspond to those against the collection
Through the above definition, the following endpoints and actions were created:
GET /api/coffee
GET /api/coffee/:c_id
POST /api/coffee
POST /api/coffee { "action": "edit" }
POST /api/coffee { "action": "delete" }
POST /api/coffee/:c_id { "action": "edit" }
POST /api/coffee/:c_id { "action": "delete" }
DELETE /api/coffee/:c_id
Where are the methods?
Now that the routes exist, let’s take care of the methods. Once the new collection has been added to the api.yml
, the corresponding controller needs to be created here. The controller will look something like:
module Api
class CoffeesController < BaseController
end
end
Because the CoffeesController
inherits from the BaseController
, a lot of what has been defined in the api.yml
will already work . The Base Controller holds the action methods (#show
, #index
, #update
, #destroy
) that will be called if they are not overridden in the CoffeesController
.
Custom Resource Actions
Once basic CRUD is established, what good is seeing a coffee if you can’t order one? A custom resource action is the solution for that. In traditional REST, this would be solved via POST /api/coffee/:c_id/order
. We instead use actions, POST /api/coffee/:c_id { "action": "order" }
With the new order action, the :resource_actions:
section will look as follows:
:resource_actions:
:get:
- :name: read
:identifier: coffee_show
:post:
- :name: edit
:identifier: coffee_edit
- :name: delete
:identifier: coffee_delete
- :name: order
:identifier: coffee_order
:delete:
- :name: delete
:identifier: coffee_delete
Since ordering a coffee isn’t a basic CRUD action handled by the generic methods, that will need to be defined in the CoffeesController
:
module Api
class CoffeesController < BaseController
def order_resource(type, id, data)
coffee = resource_search(id, type, collection_class(type))
coffee.order(data)
rescue => err
raise BadRequestError, "Coffee not ordered - #{err}"
end
end
end
The above example demonstrates a couple of the basic methods and formats that you’ll see used throughout the API.
-
order_resource(type, id, data)
-
collection_class(type)
- Based off of the
type
of request (in this case,:coffee
), it will return theklass
defined in theapi.yml
(Coffee
).
- Based off of the
-
resource_search(id, type, class)
- Resource search will return the resource corresponding to the ID that was requested, or return a
NotFoundError
if it was not found. - Resource search is used as opposed to a simple
Coffee.find(id)
because the results are filtered through access control, ensuring nobody is working with a resource they don’t have permissions for.
- Resource search will return the resource corresponding to the ID that was requested, or return a
- data
- The data input is the request body that was passed in with the order request. I like my coffee
{"cream": false, "sugar": false}
, so that is the request body that would be passed to my order.
- The data input is the request body that was passed in with the order request. I like my coffee
Subcollections
Oftentimes, we need subcollections on a resource to return child resources. Coffee is great by itself, but it’s even better with donuts. And it’s your lucky day - every coffee has a set of donuts picked to pair perfectly with it. Sounds like the perfect job for a subcollection.
Although the Donuts could also be a collection (they can still be enjoyed on their own), this example will only outline the subcollection actions for reading and deleting them.
:donuts:
:description: Donuts
:options:
- :subcollection
:verbs: *gp
:klass: Donut
:subcollection_actions:
:get:
- :name: read
:identifier: donut_show_list
:post:
- :name: delete
:identifier: donut_delete
:subresource_actions:
:get:
- :name: read
:identifier: coffee_show
:post:
- :name: delete
:identifier: coffee_delete
The above configuration is very similar to that of a collection, only it defines :subcollection_actions:
and :subresource_actions:
. The coffee options will also need to be updated to include the Donuts subcollection:
:coffees:
:description: Coffee
:options:
- :collection
:verbs: *gpd
:klass: Coffee
:subcollections:
- :donuts
Subcollection actions are defined in modules and included in the collection controller they are a subcollection of.
Below is a simple example of what the Donuts
module would look like:
module Api
module Subcollections
module Donuts
def donuts_query_resource(object)
object.donuts
end
def donuts_delete_resource(_object, type, id, data)
delete_resource(type, id, data)
end
end
end
In the above example, the object
passed to the donuts_query_resource
will be the Coffee
object that the donuts are being queried on (the parent resource to the donuts).
The two method signatures donuts_query_resource(object)
and donuts_delete_resource(object, type, id, data)
are the normal naming conventions for subcollection actions, <subcollection name>_<action>_resource
. Like the collection, you can see the target method being generated here.
The CoffeesController
then includes the Donut
module as follows:
module Api
class CoffeesController < BaseController
include Subcollections::Donuts
def order_resource(type, id, data)
coffee = resource_search(id, type, collection_class(type))
coffee.order(data)
rescue => err
raise BadRequestError, "Coffee not ordered - #{err}"
end
end
end
This subcollection now gives us the endpoints
GET /api/coffee/:c_id/donuts
GET /api/coffee/:c_id/donuts/:s_id
POST /api/coffee/:c_id/donuts/:s_id { "action": "delete" }
And just like that, our API now serves both and donuts!
Creating a PR
Small PRs are always easier to review, and this example would be broken down into two PRs - the Coffee CRUD PR and a Donuts subcollection PR. Examples in the PR description help reviewers to understand what was added, and also helps the users to get up to speed quickly! Here is a snippet of what we might include as an example for ordering a coffee:
POST /api/coffee/:id
{
"action": "order",
"cream": true,
"sugar": false
}
Questions and Examples
Below you’ll find some common questions and example PRs to guide you through the development process.
- Should this be a subcollection?
- One question seen frequently is how to determine whether something should be added as a collection, subcollection, or both. Is your resource something that you would not look at outside of the context of its parent object? If so, then a subcollection is likely the way to go.
- How are asynchronous tasks handled?
- Action results are used when an action creates an asynchronous task, and returns the task information back to the user. For example, the creation and deletion of Flavors
- Bulk actions
- Action results are also used when performing bulk actions, such as bulk tagging
- Adding subcollections
- Here is an example of taking a collection and making it into a subcollection as well
- Adding a tag subcollection to a collection is common
- Additional attributes
- Sometimes a resource may require additional attributes to be returned by default
- Overriding the generic methods
- In the event that your CRUD methods need to do something different than what the generic methods do, you can override them
Hopefully this guide was helpful in getting you up to speed on how to contribute to the API. As always, feel free to reach out to us on gitter, and leave feedback so we can write more useful blogs in the future.
We look forward to seeing your incoming PRs!