Caprese
An opinionated Rails library for creating JSONAPI servers that lets you focus on customizing the behavior of your endpoints rather than the dirty work of setting them up
Install / Use
/learn @nicklandgrebe/CapreseREADME
Caprese
Caprese is a Rails library for creating RESTful APIs in as few words as possible. It handles all CRUD operations on resources and their associations for you, and you can customize how these operations are carried out, allowing for infinite possibilities while focusing on work that matters to you, instead of writing repetitive code for each action of each resource in your application.
For now, the only format that is supported by Caprese is the JSON API schema.
Installation
Add this line to your application's Gemfile:
gem 'caprese'
And then execute:
$ bundle
Or install it yourself as:
$ gem install caprese
Philosophy
Caprese provides a controller framework that can automatically carry out index, show, create, update, and destroy actions for you with as little configuration as possible. You could write these methods yourself for every resource in your API, but the thing is, these 5 actions essentially do the same three things:
- Find a resource or set of resources, based on the parameters provided
- Optionally apply a number of changes to them, based on the data provided and the action selected
- Serialize and respond with the resource(s), in the format that was requested
Caprese does all of this dirty work for you, so all you have to do is customize its behavior to fine-tune the results. You customize the behavior using serializers, overriding methods, and defining any number of callbacks in and around the actions to fully control each step of the process outlined above.
In the real world, Caprese is a style of dish combining tomatoes, mozzarella, and basil pesto, and is usually put in a salad or on a sandwich. Just like the food, there are four components to creating an API using Caprese: models, serializers, controllers, routes.
Let's create a working API endpoint using Caprese to do something useful: allowing users to create, read, update and delete sandwiches.
Building an API for sandwiches
Prep the tomatoes (models)
class ApplicationRecord < ActiveRecord::Base
include Caprese::Record
end
# == Schema Information
#
# Table name: sandwiches
#
# id :id not null, primary key
# price :decimal not null
# description :text
# size :string(255) not null
# restaurant_id :integer not null
#
class Sandwich < ApplicationRecord
belongs_to :restaurant
has_many :condiments
end
# == Schema Information
#
# Table name: restaurants
#
# id :id not null, primary key
# name :string(255) not null
#
class Restaurant < ApplicationRecord
has_many :sandwiches
end
# == Schema Information
#
# Table name: condiments
#
# id :id not null, primary key
# name :string(255) not null
# serving_size :integer not null
# sandwich_id :integer not null
#
class Condiment < ApplicationRecord
belongs_to :sandwich
end
Tomatoes: Plain and hearty; an essential part of any true stack. The models of your application are just like them - you need them, but you can't consume them raw - your API has to decide what parts taste good for consumers. We say that models in Caprese are plain, because they're just Rails models...Caprese hasn't done much to them at all. So we create a Sandwich model with an association to a Restaurant and some Condiments and then work on giving them a better taste with serializers.
Put on the mozzarella (serializers)
class SandwichSerializer < Caprese::Serializer
attributes :price, :description, :size
belongs_to :restaurant
has_many :condiments
end
class RestaurantSerializer < Caprese::Serializer
attributes :name
end
class CondimentSerializer < Caprese::Serializer
attributes :name, :serving_size
belongs_to :sandwich
end
Mozzarella is so delicious - you can put it on anything and it's amazing. Mozzarella transforms the bland taste of tomatoes into something edible. Serializers are kinda the same way - you can use them to take a complex data model and turn it into something more consumable for people: JSON. When a user requests a sandwich from our API, Caprese will use the serializers above to define the fields (attributes and relationships) that the user sees, and by default, the response will look something like this:
{
"data": {
"type": "sandwiches",
"id": "1",
"attributes": {
"price": 10.0,
"description": "Tomato, mozzarella, and basil pesto between two pieces of bread.",
"size": "large"
},
"relationships": {
"condiments": {
"data": [
{ "type": "condiments", "id": "5" },
{ "type": "condiments", "id": "6" }
]
},
"restaurant": {
"data": {
"type": "restaurants",
"id": "2"
}
}
}
}
}
NOTE: Caprese only includes resource identifiers (type and id) for the condiments and restaurant of the sandwich, or any other relationship for that matter. It does not include the fields (attributes and relationships) of these resources unless the user specifically requests them (see this section of JSON API format for details).
Bring the tomato and mozzarella together onto a sandwich or salad (controllers)
The bread of a sandwich or the leaves of a salad are what bring the entire Caprese dish together. Controllers are the same way - alongside tomatoes they are the "bite" of our application. When someone asks for a sandwich from our API, a controller fulfills the request, providing a necessary platform for that user to consume our tomatoes and mozzarella (the serialized resources). Let's bring our sandwich endpoint together with a controller, configuring it so it understands what information to use when creating a sandwich requested by a user:
class SandwichesController < Caprese::Controller
def permitted_create_params
[
:size, :condiments, :restaurant
]
end
end
This means that when a user requests a sandwich, we will use the size of the sandwich, any condiments, as well as the restaurant that the user specified in order to create a new sandwich. Note that we don't include price and description - we don't want the user to be able to change these. The request that the user makes will look something like this:
{
"data": {
"type": "sandwiches",
"attributes": {
"size": "small"
},
"relationships": {
"condiments": {
"data": [
{ "type": "condiments", "id": "5" },
{ "type": "condiments", "id": "6" },
]
},
"restaurant": {
"data": {
"type": "restaurants",
"id": "1"
}
}
}
}
}
You could also let the user create new condiments that aren't on the menu and put them onto their sandwich. Your controller would have to look like this:
class SandwichesController < Caprese::Controller
def permitted_create_params
[
:size, :restaurant,
condiments: [:name, :serving_size]
]
end
end
Now, the controller will look at the name and serving_size attributes of each condiment when creating the sandwich, and add each new condiment to the end result. The request the user would make would look like this:
{
"data": {
"type": "sandwiches",
"attributes": {
"size": "small"
},
"relationships": {
"condiments": {
"data": [
{
"type": "condiments",
"attributes": {
"name": "Dragon Blood",
"serving_size": "2"
}
},
{
"type": "condiments",
"attributes": {
"name": "Deep Fried Pickles",
"serving_size": "10"
}
}
]
},
"restaurant": {
"data": {
"type": "restaurants",
"id": "1"
}
}
}
}
}
The response (outlined below) would contain the created sandwich along with any newly created condiments. Note that the attributes of the condiments that the user specified are not returned; remember that Caprese does not respond with attributes and relationships of related resources unless specifically told to do so.
{
"data": {
"type": "sandwiches",
"id": "1",
"attributes": {
"price": 5.0,
"description": "Tomato, mozzarella, and basil pesto between two pieces of bread.",
"size": "small"
},
"relationships": {
"condiments": {
"data": [
{ "type": "condiments", "id": "10" },
{ "type": "condiments", "id": "11" },
]
},
"restaurant": {
"data": {
"type": "restaurants",
"id": "1"
}
}
}
}
}
If you want users to be able to update sandwiches they've already created, you must also specify what they are allowed to update in the same manner as create:
class SandwichesController < Caprese::Controller
def permitted_create_params
[
:size, :restaurant,
condiments: [:name, :serving_size]
]
end
# Only allow users to change the condiments of their sandwich
# 1. Don't let them update the sandwich by creating new condiments, only specifying existing ones
# 2. Don't let them change the size or the restaurant
def permitted_update_params
[
:condiments
]
end
end
Complete with a dollop of basil pesto (routes)
All that's left to complete our sandwich API is to add routes for index, show, create, update, and destroy:
Rails.application.routes.draw do
caprese_res
