In this tutorial, we’ll create Store API endpoints for our Brand model so that storefronts can list and display brands. We’ll also extend the existing Product serializer to include brand data.Documentation Index
Fetch the complete documentation index at: https://spreecommerce.org/docs/llms.txt
Use this file to discover all available pages before exploring further.
This guide assumes you’ve completed the Model, Admin, and Extending Core Models tutorials.
What We’re Building
By the end of this tutorial, you’ll have:GET /api/v3/store/brands— list all brandsGET /api/v3/store/brands/:id— get a single brand by prefixed ID or slug- Brand data included in Product responses via
?expand=brand - Understanding of how to add new API endpoints and extend existing serializers
How the Store API Works
Every Store API endpoint follows the same pattern:- Controller inherits from
Spree::Api::V3::Store::ResourceControllerwhich provides CRUD, pagination, Ransack filtering, and authorization out of the box - Serializer inherits from
Spree::Api::V3::BaseSerializer(uses Alba) and defines which fields to return - Routes are added via
Spree::Core::Engine.add_routes - Serializer registration via
Spree::Api::Dependenciesenables dependency injection so serializers can be swapped by extensions or the host app
Step 1: Prepare the Brand Model for the API
Store API requires two things from models:- Prefixed IDs — Stripe-style IDs like
brand_k5nR8xLqinstead of raw database IDs - Slugs — human-readable URL identifiers like
nikeforGET /brands/nike
slug column if you haven’t already:
PrefixedId and FriendlyId:
app/models/spree/brand.rb
Spree::Brand.first.prefixed_idreturnsbrand_k5nR8xLqSpree::Brand.find_by_prefix_id!('brand_k5nR8xLq')finds by prefixed IDSpree::Brand.friendly.find('nike')finds by slug- Slugs are auto-generated from the
nameviaslug_candidates(inherited fromSpree::Base)
Step 2: Create the Serializer
Create a serializer that defines the JSON response shape for brands:app/serializers/spree/api/v3/brand_serializer.rb
Understanding the Serializer
BaseSerializerautomatically convertsidto a prefixed ID and provides context helpers (current_store,current_currency, etc.)typelizeprovides type hints used by Typelizer to auto-generate TypeScript types for the SDKattributeslists database columns to include directlyattribute ... doblocks define computed fields (like stripping HTML from rich text, or generating image URLs)
Step 3: Create the Controller
Create a controller that inherits fromStore::ResourceController:
app/controllers/spree/api/v3/store/brands_controller.rb
Understanding the Controller
ResourceController gives you index and show actions automatically. You only need to define:
| Method | Purpose |
|---|---|
model_class | Which ActiveRecord model to query |
serializer_class | Which serializer to render responses with |
scope | Base query scope (add .where(...) to filter) |
- Pagination via Pagy (
?page=2&limit=25) - Filtering via Ransack (
?q[name_cont]=nike) - Sorting via JSON:API style (
?sort=-namefor descending) - Authorization via CanCanCan
- Prefixed ID lookup for
showaction (/brands/brand_k5nR8xLq)
For core models, controllers use
Spree.api.product_serializer which looks up the serializer from Spree::Api::Dependencies. This allows extensions to swap the serializer. For your own custom models, reference the serializer class directly — the dependency system only supports core injection points.Adding Slug Lookup
To also support fetching brands by slug (like products support/products/blue-t-shirt), override find_resource:
app/controllers/spree/api/v3/store/brands_controller.rb
Step 4: Add Routes
Add the routes for your new endpoints:config/routes.rb
GET /api/v3/store/brands— paginated list with filtering/sortingGET /api/v3/store/brands/:id— single brand by prefixed ID or slug
Step 5: Test the Endpoints
Restart your server and test:Response Format
List response:Step 6: Add Brand to Product Responses
Now let’s extend the Product serializer so that brand data is included when a storefront requests?expand=brand.
Create a Custom Product Serializer
Subclass the coreProductSerializer and add brand fields. Then swap it in via Dependencies:
app/serializers/my_app/product_serializer.rb
config/initializers/spree.rb
Understanding the Serializer
brand_id— always included as a flat attribute (prefixed ID string), so storefronts know which brand a product belongs to without expandingone :brand— conditionally included when the client requests?expand=brand, returns the full brand object inlineexpand?('brand')— checks if theexpandquery parameter includes'brand'
We subclass and swap via
Spree::Api::Dependencies rather than using a decorator. This is the recommended pattern for customizing core serializers — it’s explicit, easy to test, and other extensions can further subclass your serializer.How Expand Works
The expand system keeps responses lean by default and lets clients opt-in to nested data:?expand=brand:
Extending Core Serializers (General Pattern)
The pattern we used for Product works for any core serializer. Subclass the core serializer, add your fields, and swap it in viaSpree::Api::Dependencies:
app/serializers/my_app/product_serializer.rb
config/initializers/spree.rb
Spree::Api::ApiDependencies for the full list). Your subclass inherits all existing attributes and associations, and other extensions can further subclass yours.
Complete Files
Brand Model
app/models/spree/brand.rb
Brand Serializer
app/serializers/spree/api/v3/brand_serializer.rb
Brands Controller
app/controllers/spree/api/v3/store/brands_controller.rb
Custom Product Serializer
app/serializers/my_app/product_serializer.rb
Routes
config/routes.rb
Initializer
config/initializers/spree.rb
Related Documentation
- Extending Core Models - Adding the Brand association to Product
- Using Brands with the SDK - Consuming brand endpoints from TypeScript
- Decorators - Full decorator reference
- Dependencies - Swapping services and serializers

