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

