-
Notifications
You must be signed in to change notification settings - Fork 21
Writing an API Method
Stitches is designed to allow you to write your API in a standard Rails-like fashion. However you write Rails is how you write a Stitches-based API.
The way to think about your API is to do the same as for any Rails feature:
- What is the resource being manipulated?
- Create a route and controller for that resource
- Implement the necessary restful methods
- Use Rails as much as possible
Let's see what that looks like. Let's implement an API to for orders in an e-commerce system.
Our resource is an "Order", so it goes in OrdersController
.
The Rails way to create an order is to implement the create
method which means our app responds to a POST
to /orders
.
class OrdersController < ApiController
def create
order = Order.create(order_params)
if order.valid?
render json: { order: order }, status: 201
else
render json: Stitches::Errors.from_active_record(order),
status: 422
end
end
private
def order_params
params.require(:order).permit!(:customer_id, :items, :address)
end
end
This should be the way you start every implementation. Always ask if you can just do it the regular Rails way. The only difference between this and vanilla rails is that we are rendering JSON, and using Stitches error handling.
Note that our JSON format is simply whatever to_json
would produce. You may think we are "just exposing our database", but this is part of Rails' design. Our API reflects our domain, which is also reflected in our database design. Just because it mirrors a database table now doesn't mean it has to in the future. You should have tests to cover your API anyway, and they will detect if you modify your database in a way that breaks your API.
Also note that we aren't recommending something like Active Model Serializers here. Ideally there is exactly one representation of your domain model, and that representation is, by default, to_json
, as implemented by Rails. JSON serialization may be a bottleneck for you someday, but that day is not today. Don't complicate your life with fancy JSON libraries unless you need them. If you need something custom, it's very easy to customize the JSON.
Next, we need to view an order.
rescue_from ActiveRecord::NotFoundError do |ex|
render json: { errors: Stitches::Errors.from_exception(ex) }, status: 404
end
def show
order = Order.find(params[:id])
render json: { order: order }
end
Note again that this is just vanilla rails. Also note that we have a top-level object. This avoids oddities if this JSON is sent to a browser, but also allows consumers to know what they are receiving without any context.
Finally, note that we are handling errors in the standard Rails Way, but using rescue_from
so we can send a structured error message. You may want to put that rescue_from
in ApiController
so you don't have to repeat it.
Editing an order is much like creating one, but for completeness, let's implement that
def update
order = Order.find(params[:id])
order.update(order_params)
if order.valid?
render json: { order: order }, status: 200
else
render json: Stitches::Errors.from_active_record(order),
status: 422
end
end
This capitalizes on the rescue_from
we implemented before. Also note we return 200 here, but 201 in create
.
Deleting an order is also simple:
def destroy
order = Order.find(params[:id])
order.destroy
head :ok
end
This is even simpler than the others since we don't need to return anything. The caller can fetch the order it's about to delete if it wants, but this is not necessary typically.
This brings us to listing orders, which is the index
method.
def index
orders = Order.all
render json: { orders: orders }
end
Again, we just use Rails. The top-level object is pluralized, and is important, since sending naked arrays can create weird bugs in JavaScript.
You probably don't want to return the entire database. To do pagination, this would be a minimal implementation
def index
page_size = (params[:page_size] || 20).to_i # remember, params are strings
page_num = (params[:page_num] || 0).to_i # remember, params are strings
orders = Order.all.order(:id).offset(page_num * page_size).limit(page_size)
render json: {
orders: orders,
meta: {
page_size: page_size,
page_num: page_num,
}
}
end
You can get fancier if you like. Note how the use of a top-level object allows us to side-load some metadata about the pagination.
In some cases, the resource you are exposing is not a database table. Sometimes it is, and that's fine, but if it's not, you have a few options.
You can implement your API exactly as above by creating an Active Record-like object using ActiveModel. Suppose our Order use-case is exactly the same, but we don't have an ORDERS
table. Suppose we have a SHIPMENTS
table and a SHIPPING_ADDRESSES
table.
class Order
include ActiveModel::Model
attr_accessor :id, :customer_id, :address, :line_items
def self.find(id)
shipment = Shipment.find(id)
self.new(
id: shipment.id,
customer_id: shipment.customer.id,
address: shipment.customer.shipping_address,
line_items: shipment.items
)
end
def self.all
# similarly
end
def self.create(params)
# whatever
end
def update
# whatever
end
def destroy
# whatever
end
end
Because we're using ActiveModel, everything you'd expect from a real Active Record more or less just works.
Here, we make two classes, one that holds the data for our resource and one that has all the logic. This is more useful when you only support a few operations. Suppose our Order API only allows creating and viewing a single order. We might create OrderService
like so:
class OrderService
def create_order(params)
end
def find(id)
end
end
And then Order
can be an ActiveModel if we need all the validation goodies like above, or if we dont' need that, we can use an ImmutableStruct
require "immutable-struct"
Order = ImmutableStruct.new(
:id,
:customer_id,
:address,
[:line_items]
)
Our controller code would be a bit less idiomatic, but still straightforward:
class OrdersController < ApiController
rescue_from ActiveRecord::NotFoundError do |ex|
render json: { errors: Stitches::Errors.from_exception(ex) }, status: 404
end
def show
order = order_service.find(params[:id])
render json: { order: order }
end
def create
order = order_service.create_order(order_params)
if order.valid?
render json: { order: order }, status: 201
else
render json: Stitches::Errors.from_active_record(order),
status: 422
end
end
private
def order_service
@order_service ||= OrderService.new
end
def order_params
params.require(:order).permit!(:customer_id, :items, :address)
end
end
In any case, you don't need any more code in your controller than you'd need in a regular Rails app. That's the point of Stitches.