Customizing the API
This guide will teach you how to customize Solidus' REST API.
Introduction to the REST API
We also provide a GraphQL API, if you're more into that. Check it out!
Solidus comes with a complete REST API that allows you to manage all aspects of your store. In some cases, you may want to extend the API's functionality by adding new endpoints and customizing the existing ones. Like all other parts of Solidus, the API is a Rails engine, which means you can use the regular tools at your disposal to customize its behavior.
The API is also used by the default Solidus backend to perform certain tasks without reloading the page. If you are modifying an endpoint's input schema or heavily customizing its output, make sure you are not accidentally breaking your backend! If in doubt, you can always create a new endpoint for your purposes.
In this guide, we'll see how to implement a quick-and-dirty API for customers to "like" a certain
product, so that you know which products customer like the most. We'll implement a new endpoint for
liking a product and we'll add a likes_count
attribute to the
existing products API.
Let's get started!
Designing your resources
First of all, we need to make sure users can somehow tell us that they like a product. Because we want this to happen without users having to reload the page, we'll do it via the API.
We could just add a likes_count
column to the spree_products
table, but in our case we also want
to know which users liked which products, so that a product cannot be liked twice by the same user
and we can personalize the user's recommendations based on the products they liked. We will also
want to make sure that unauthenticated users cannot like a product, because we wouldn't be able to
associate that like to a specific customer.
Let's start by creating a new model, ProductLike
, which we'll use to store the user-product
relationship:
$ rails g model ProductLike user:belongs_to product:belongs_to
We'll need to edit the model generated by this command to specify the full namespace of our associated models:
class ProductLike < ApplicationRecord
belongs_to :product, class_name: 'Spree::Product'
belongs_to :user, class_name: 'Spree::User'
end
We'll also have to edit the migration manually to specify the correct table names for the foreign keys:
class CreateProductLikes < ActiveRecord::Migration[6.0]
def change
create_table :product_likes do |t|
t.integer :user_id, null: false
t.integer :product_id, null: false
t.foreign_key :spree_users, column: :user_id, on_delete: :cascade
t.foreign_key :spree_products, column: :product_id, on_delete: :cascade
t.timestamps
end
end
end
Once we're done, we can run the migration with the usual command:
$ rails db:migrate
Finally, we will add a uniqueness validation to our model:
class ProductLike < ApplicationRecord
# ...
validates :user, uniqueness: { scope: :product_id }
end
Now that we have our model in place, we're ready to start creating an API endpoint for liking products!
Creating a new endpoint
Implementing a new API endpoint is fairly simple: all we have to do is create a new controller along
with its actions, routes and views, just like we would do in a regular Rails application. For our
use case, we'll want to create a ProductLikesController
with a create
action that allows us to
like a product.
We could also add a like
action to the existing Spree::Api::ProductsController
. However, it's
recommended to follow RESTful resource naming when creating new API endpoints. This makes our API
easier to understand, consume and maintain.
Let's create our controller first:
module Spree
module Api
class ProductLikesController < Spree::Api::BaseController
def create
@product = find_product(params[:product_id])
@like = ProductLike.new(product: @product, user: current_api_user)
if @like.save
render :show, status: :no_content
else
invalid_resource!(@like)
end
end
end
end
end
In our controller, we are relying on a few helpers Solidus provides out of the box:
find_product
retrieves a product by its numeric ID or slug.current_api_user
retrieves the currently authenticated user (by default, all API requests require authentication).invalid_resource!
generates a 422 response that exposes all the error messages on an ActiveModel-compliant object.
Now that we have the controller, let's also create the corresponding view:
json.user_id(@like.user_id)
json.partial!("spree/api/products/product", product: @like.product)
In the view, we are using JBuilder to create a JSON
representation of our ProductLike
instance. The user ID is represented as an integer, while the
product is transformed into a full JSON object by using Solidus' _product.json.jbuilder
partial.
And finally, let's add a route to the controller action:
# ...
Spree::Core::Engine.routes.draw do
namespace :api, defaults: { format: 'json' } do
resources :products do
resource :product_like, only: :create
end
end
end
In the route, you may notice we are using a singleton resource (resource :product_like
) rather
than a collection (resource :product_likes
). This is because a user may only have one like for a
product. We are also limiting the routes for that resource to the :create
action, since we are not
going to implement the others.
Solidus provides a lot more partials, helpers and utilities for implementing your API requests. Take a look at the source code of solidus_api to see what's available!
At this point, your request spec should be passing, meaning you can integrate it in your frontend and allow users to like products!
Extending existing resources
As a next step, we'll add a likes_count
to the Spree::Product
model and expose it in our API. In
order to do this, we first need to add the column to the spree_products
table:
$ rails g migration AddLikesCountToSpreeProducts likes_count:integer
We will also need to make sure the likes_count
column is automatically updated with the number of
users who have liked the product. In order to do this, we can use ActiveRecord's
excellent counter cache feature
. Let's modify the belongs_to :product
association by enabling the option:
class ProductLike < ApplicationRecord
# ...
belongs_to :product, class_name: 'Spree::Product', counter_cache: :likes_count
# ...
end
ActiveRecord should now start keeping the number of likes in the likes_count
column.
In order to expose this field to API clients, we'll need to add a JSON field to the products API. We
could just copy-paste the product.json.jbuilder
view from Solidus and add the field there, but
then we would need to remember to update our custom view every time the original view is changed.
Instead, Solidus provides a more manageable way to add attributes to API resources via
the ApiHelpers
module. Let's see how we can do it and test it:
# ...
Spree::Api::Config.product_attributes << :likes_count
That's all we need! We have created a new API resource and implemented a new endpoint to manipulate it, and we have seen how to add fields to an existing API resource. If you feel adventurous, how about trying to implement an endpoint for removing an existing product like?