Skip to main content
Version: 3.3

Customizing the API

This guide will teach you how to customize Solidus' REST API.

Introduction to the REST API

info

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.

danger

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:

app/models/product_like.rb
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:

app/models/product_like.rb
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.

info

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:

app/controllers/spree/api/product_likes_controller.rb
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:

app/views/spree/api/product_likes/show.json.jbuilder
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:

config/routes.rb
# ...
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.

info

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:

config/initializers/spree.rb
# ...
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?

How-to guides