Skip to content

Writing an API Method

J Jesús Abarca Vargas edited this page Aug 13, 2019 · 4 revisions

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.

Canonical create method

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.

Canonical show method

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

Canonical update method

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:

Canonical destroy method

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.

Canonical 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.

What If There is No Active Record?

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.

Option 1 - Create an ActiveRecord-like object

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.

Option 2 - Use a service class and presenter

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.