Solidus
Search…
Customizing the 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 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:
1
$ rails g model ProductLike user:belongs_to product:belongs_to
Copied!
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
1
class ProductLike < ApplicationRecord
2
belongs_to :product, class_name: 'Spree::Product'
3
belongs_to :user, class_name: 'Spree::User'
4
end
Copied!
We'll also have to edit the migration manually to specify the correct table names for the foreign keys:
1
class CreateProductLikes < ActiveRecord::Migration[6.0]
2
def change
3
create_table :product_likes do |t|
4
t.integer :user_id, null: false
5
t.integer :product_id, null: false
6
7
t.foreign_key :spree_users, column: :user_id, on_delete: :cascade
8
t.foreign_key :spree_products, column: :product_id, on_delete: :cascade
9
10
t.timestamps
11
end
12
end
13
end
Copied!
Once we're done, we can run the migration with the usual command:
1
$ rails db:migrate
Copied!
Finally, we will add a uniqueness validation to our model:
app/models/product_like.rb
1
class ProductLike < ApplicationRecord
2
# ...
3
validates :user, uniqueness: { scope: :product_id }
4
end
Copied!
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, along with its view and route:
product_likes_controller.rb
show.json.jbuilder
routes.rb
app/controllers/spree/api/product_likes_controller.rb
1
module Spree
2
module Api
3
class ProductLikesController < Spree::Api::BaseController
4
def create
5
@product = find_product(params[:product_id])
6
@like = ProductLike.new(product: @product, user: current_api_user)
7
8
if @like.save
9
render :show, status: :no_content
10
else
11
invalid_resource!(@like)
12
end
13
end
14
end
15
end
16
end
Copied!
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.
app/views/spree/api/product_likes/show.json.jbuilder
1
json.user_id(@like.user_id)
2
json.partial!("spree/api/products/product", product: @like.product)
Copied!
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.
config/routes.rb
1
# ...
2
Spree::Core::Engine.routes.draw do
3
namespace :api, defaults: { format: 'json' } do
4
resources :products do
5
resource :product_like, only: :create
6
end
7
end
8
end
Copied!
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:
1
$ rails g migration AddLikesCountToSpreeProducts likes_count:integer
Copied!
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:
1
class ProductLike < ApplicationRecord
2
# ...
3
belongs_to :product, class_name: 'Spree::Product', counter_cache: :likes_count
4
# ...
5
end
Copied!
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.rb
config/initializers/spree.rb
1
# ...
2
Spree::Api::Config.product_attributes << :likes_count
Copied!
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?
Last modified 3mo ago