Skip to main content
This is a backend customization. You need to perform it in the backend directory if you used create-spree-app to set up your Spree application.
In this tutorial, we’ll expose our Brand model through Spree’s v3 API — the customer-facing Store API that storefronts read from, and the back-office Admin API with full CRUD for apps and integrations. We’ll also extend the existing Product serializer to include brand data.
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 and GET /api/v3/store/brands/:id — customer-facing, read-only, lookup by prefixed ID or slug
  • Full CRUD on /api/v3/admin/brands — for back-office apps and integrations
  • Brand data included in Product responses via ?expand=brand
  • Understanding of how to add new API endpoints and extend existing serializers

The Fast Path: One Generator Command

Everything this page builds by hand can be generated in one command with spree:api_resource:
Because the Brand model already exists, the generator leaves it (and its migration) untouched and produces only the API surface — no conflict prompts, no overwrites. Your model is “owned once”: after creation, domain code belongs to you, and the generator only ever adds API files around it. You’ll see this in the output:
For a brand-new resource you’d pass the attributes too (spree generate api_resource Brand name:string:uniq) and get the model and migration in the same run. If you just want a working API, run the generator and skip ahead to Step 5: Test the Endpoints. The rest of this page builds the Store side by hand so you understand what the generator produces and how to customize it — the Admin API section then shows how little the back-office surface adds on top.
Using an AI agent? The Spree agent skills include a dedicated resource-generator skill — your agent knows the field syntax, the flags, and the generated-file contract.

How the Store API Works

Every Store API endpoint follows the same pattern:
  1. Controller inherits from Spree::Api::V3::Store::ResourceController which provides CRUD, pagination, Ransack filtering, and authorization out of the box
  2. Serializer inherits from Spree::Api::V3::BaseSerializer (uses Alba) and defines which fields to return
  3. Routes are added via Spree::Core::Engine.add_routes
  4. Serializer registration via Spree::Api::Dependencies enables 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:
  1. Prefixed IDs — Stripe-style IDs like brand_k5nR8xLq instead of raw database IDs. The spree:model generator already added has_prefix_id :brand in the Model step, so this is done.
  2. Slugs — human-readable URL identifiers like nike for GET /brands/nike
Add a slug column:
Then add FriendlyId to the Brand model:
app/models/spree/brand.rb
Now:
  • Spree::Brand.first.prefixed_id returns brand_k5nR8xLq
  • Spree::Brand.find_by_prefix_id!('brand_k5nR8xLq') finds by prefixed ID
  • Spree::Brand.friendly.find('nike') finds by slug
  • Slugs are auto-generated from the name via slug_candidates (inherited from the Spree base class)

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

  • BaseSerializer automatically converts id to a prefixed ID and provides context helpers (current_store, current_currency, etc.)
  • typelize provides type hints used by Typelizer to auto-generate TypeScript types for the SDK
  • attributes lists database columns to include directly
  • attribute ... do blocks define computed fields (like stripping HTML from rich text, or generating image URLs)

Step 3: Create the Controller

Create a controller that inherits from Store::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: The base controller handles:
  • Pagination via Pagy (?page=2&limit=25)
  • Filtering via Ransack (?q[name_cont]=nike)
  • Sorting via JSON:API style (?sort=-name for descending)
  • Authorization via CanCanCan
  • Prefixed ID lookup for show action (/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
This creates:
  • GET /api/v3/store/brands — paginated list with filtering/sorting
  • GET /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:

The Admin API

The Admin API is the other half of v3 — same protocol, same serializer/controller patterns, but authenticated with secret keys (sk_*) or admin JWTs, and full CRUD by default. The spree:api_resource generator produces both pieces; here’s what they look like:
app/serializers/spree/api/v3/admin/brand_serializer.rb
app/controllers/spree/api/v3/admin/brands_controller.rb
Two conventions to notice:
  • The Admin serializer extends the Store serializer — public fields stay in sync automatically, and the Admin side adds back-office data (timestamps here; cost prices, internal notes, and audit fields on richer resources). Customers never see those fields because storefronts use the Store serializer.
  • Admin::ResourceController ships full CRUDindex, show, create, update, and destroy are inherited; permitted_params lists the writable attributes with flat params (no nested brand: {...} wrapping).
With the routes registered (resources :brands under the admin namespace — the generator injects this), back-office clients get:
Secret keys carry scopes (read_brands, write_brands style) and JWT admin users go through CanCanCan abilities — see API authentication for the full model. From TypeScript, the Admin SDK wraps the Admin API with typed clients for all built-in resources.

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 core ProductSerializer and add brand fields. Then swap it in via Dependencies:
app/serializers/my_app/product_serializer.rb
Register it and whitelist the brand association for Ransack filtering in your initializer:
config/initializers/spree.rb
Without Spree.ransack.add_association, Ransack predicates like brand_name_cont will be silently ignored. Spree whitelists ransackable attributes and associations on each model — custom ones must be registered explicitly.

Understanding the Serializer

  • brand_id — always included as a flat attribute (prefixed ID string), so storefronts know which brand a product belongs to without expanding
  • one :brand — conditionally included when the client requests ?expand=brand, returns the full brand object inline
  • expand?('brand') — checks if the expand query 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:
Response with ?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 via Spree::Api::Dependencies:
app/serializers/my_app/product_serializer.rb
config/initializers/spree.rb
This works for any core serializer registered in Dependencies (see 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