> ## 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.

# Expose a custom model through the Store and Admin APIs

> Expose your custom Brand model through the Spree Store and Admin REST APIs with serializers, controllers, routes, and the spree:api_resource generator.

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.

<Info>
  This guide assumes you've completed the [Model](/developer/tutorial/model), [Admin](/developer/tutorial/admin), and [Extending Core Models](/developer/tutorial/extending-models) tutorials.
</Info>

## 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`:

<CodeGroup>
  ```bash Spree CLI (Docker) theme={"theme":"night-owl"}
  spree generate api_resource Brand
  ```

  ```bash Without Spree CLI theme={"theme":"night-owl"}
  bin/rails g spree:api_resource Brand
  ```
</CodeGroup>

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:

```text theme={"theme":"night-owl"}
      skip  model app/models/spree/brand.rb (owned-once; already exists)
      skip  migration (model already exists; add a new migration for schema changes)
    create  app/controllers/spree/api/v3/store/brands_controller.rb
    create  app/controllers/spree/api/v3/admin/brands_controller.rb
    create  app/serializers/spree/api/v3/brand_serializer.rb
    create  app/serializers/spree/api/v3/admin/brand_serializer.rb
    create  spec/factories/spree/brand_factory.rb
```

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](#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](#the-admin-api) then shows how little the back-office surface adds on top.

<Tip>
  Using an AI agent? The [Spree agent skills](/developer/agentic/agent-skills) include a dedicated resource-generator skill — your agent knows the field syntax, the flags, and the generated-file contract.
</Tip>

## 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](https://github.com/okuramasafumi/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](/developer/tutorial/model), so this is done.
2. **Slugs** — human-readable URL identifiers like `nike` for `GET /brands/nike`

Add a `slug` column:

<CodeGroup>
  ```bash Spree CLI (Docker) theme={"theme":"night-owl"}
  spree generate migration AddSlugToSpreeBrands slug:string:uniq
  spree migrate
  ```

  ```bash Without Spree CLI theme={"theme":"night-owl"}
  bin/rails g migration AddSlugToSpreeBrands slug:string:uniq
  bin/rails db:migrate
  ```
</CodeGroup>

Then add `FriendlyId` to the Brand model:

```ruby app/models/spree/brand.rb {3,6} theme={"theme":"night-owl"}
module Spree
  class Brand < Spree.base_class
    extend FriendlyId

    has_prefix_id :brand
    friendly_id :slug_candidates, use: [:slugged, :scoped], scope: spree_base_uniqueness_scope

    has_many :products, class_name: 'Spree::Product', dependent: :nullify

    has_rich_text :description
    has_one_attached :logo

    validates :name, presence: true

    self.whitelisted_ransackable_attributes = %w[name]
    self.whitelisted_ransackable_associations = %w[]
    self.whitelisted_ransackable_scopes = %w[]
  end
end
```

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:

```ruby app/serializers/spree/api/v3/brand_serializer.rb theme={"theme":"night-owl"}
module Spree
  module Api
    module V3
      class BrandSerializer < BaseSerializer
        typelize name: :string,
                 slug: [:string, nullable: true],
                 description: [:string, nullable: true],
                 logo_url: [:string, nullable: true]

        attributes :name, :slug

        attribute :description do |brand|
          brand.description&.to_plain_text
        end

        attribute :logo_url do |brand|
          image_url_for(brand.logo) if brand.logo.attached?
        end
      end
    end
  end
end
```

### 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](https://github.com/skryukov/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`:

```ruby app/controllers/spree/api/v3/store/brands_controller.rb theme={"theme":"night-owl"}
module Spree
  module Api
    module V3
      module Store
        class BrandsController < ResourceController
          protected

          def model_class
            Spree::Brand
          end

          def serializer_class
            Spree::Api::V3::BrandSerializer
          end

          def scope
            Spree::Brand.all
          end
        end
      end
    end
  end
end
```

### 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) |

The base controller handles:

* **Pagination** via [Pagy](https://github.com/ddnexus/pagy) (`?page=2&limit=25`)
* **Filtering** via [Ransack](https://github.com/activerecord-hackery/ransack) (`?q[name_cont]=nike`)
* **Sorting** via JSON:API style (`?sort=-name` for descending)
* **Authorization** via [CanCanCan](https://github.com/CanCanCommunity/cancancan)
* **Prefixed ID lookup** for `show` action (`/brands/brand_k5nR8xLq`)

<Info>
  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.
</Info>

### Adding Slug Lookup

To also support fetching brands by slug (like products support `/products/blue-t-shirt`), override `find_resource`:

```ruby app/controllers/spree/api/v3/store/brands_controller.rb {12-19} theme={"theme":"night-owl"}
module Spree
  module Api
    module V3
      module Store
        class BrandsController < ResourceController
          protected

          def model_class
            Spree::Brand
          end

          def find_resource
            id = params[:id]
            if id.to_s.start_with?('brand_')
              scope.find_by_prefix_id!(id)
            else
              scope.friendly.find(id)
            end
          end

          def serializer_class
            Spree::Api::V3::BrandSerializer
          end

          def scope
            Spree::Brand.all
          end
        end
      end
    end
  end
end
```

## Step 4: Add Routes

Add the routes for your new endpoints:

```ruby config/routes.rb theme={"theme":"night-owl"}
Spree::Core::Engine.add_routes do
  namespace :api, defaults: { format: 'json' } do
    namespace :v3 do
      namespace :store do
        resources :brands, only: [:index, :show]
      end
    end
  end
end
```

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:

```bash theme={"theme":"night-owl"}
# List brands
curl -H "X-Spree-API-Key: pk_YOUR_KEY" \
     http://localhost:3000/api/v3/store/brands

# Get a single brand
curl -H "X-Spree-API-Key: pk_YOUR_KEY" \
     http://localhost:3000/api/v3/store/brands/brand_k5nR8xLq

# Filter by name
curl -H "X-Spree-API-Key: pk_YOUR_KEY" \
     "http://localhost:3000/api/v3/store/brands?q[name_cont]=nike"

# Sort alphabetically
curl -H "X-Spree-API-Key: pk_YOUR_KEY" \
     "http://localhost:3000/api/v3/store/brands?sort=name"
```

### Response Format

List response:

```json theme={"theme":"night-owl"}
{
  "data": [
    {
      "id": "brand_k5nR8xLq",
      "name": "Nike",
      "slug": "nike",
      "description": "Just Do It",
      "logo_url": "https://cdn.example.com/brands/nike-logo.png"
    }
  ],
  "meta": {
    "page": 1,
    "limit": 25,
    "count": 42,
    "pages": 2,
    "from": 1,
    "to": 25,
    "in": 25,
    "previous": null,
    "next": 2
  }
}
```

## 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:

```ruby app/serializers/spree/api/v3/admin/brand_serializer.rb theme={"theme":"night-owl"}
module Spree
  module Api
    module V3
      module Admin
        class BrandSerializer < V3::BrandSerializer
          attributes :created_at, :updated_at
        end
      end
    end
  end
end
```

```ruby app/controllers/spree/api/v3/admin/brands_controller.rb theme={"theme":"night-owl"}
module Spree
  module Api
    module V3
      module Admin
        class BrandsController < ResourceController
          protected

          def model_class
            Spree::Brand
          end

          def serializer_class
            Spree::Api::V3::Admin::BrandSerializer
          end

          def permitted_params
            params.permit(:name)
          end
        end
      end
    end
  end
end
```

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 CRUD** — `index`, `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:

```bash theme={"theme":"night-owl"}
# Create a brand with a secret API key
curl -X POST -H "X-Spree-API-Key: sk_YOUR_KEY" \
     -H "Content-Type: application/json" \
     -d '{"name": "Adidas"}' \
     http://localhost:3000/api/v3/admin/brands
```

<Info>
  Secret keys carry scopes (`read_brands`, `write_brands` style) and JWT admin users go through CanCanCan abilities — see [API authentication](/developer/customization/api) for the full model. From TypeScript, the [Admin SDK](/developer/sdk/admin/quickstart) wraps the Admin API with typed clients for all built-in resources.
</Info>

## 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:

```ruby app/serializers/my_app/product_serializer.rb theme={"theme":"night-owl"}
module MyApp
  class ProductSerializer < Spree::Api::V3::ProductSerializer
    typelize brand_id: [:string, nullable: true]

    attribute :brand_id do |product|
      product.brand&.prefixed_id
    end

    one :brand,
        resource: Spree::Api::V3::BrandSerializer,
        if: proc { expand?('brand') }
  end
end
```

Register it and whitelist the brand association for Ransack filtering in your initializer:

```ruby config/initializers/spree.rb theme={"theme":"night-owl"}
# Swap in custom product serializer with brand support
Spree::Api::Dependencies.product_serializer = 'MyApp::ProductSerializer'

# Allow filtering products by brand (e.g., ?q[brand_name_cont]=nike or ?q[brand_id_eq]=123)
Spree.ransack.add_attribute(Spree::Product, :brand_id)
Spree.ransack.add_association(Spree::Product, :brand)
```

<Warning>
  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.
</Warning>

### 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'`

<Info>
  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.
</Info>

### How Expand Works

The expand system keeps responses lean by default and lets clients opt-in to nested data:

```bash theme={"theme":"night-owl"}
# Without expand — brand_id only
GET /api/v3/store/products/prod_86Rf07xd4z

# With expand — full brand object included
GET /api/v3/store/products/prod_86Rf07xd4z?expand=brand

# Multiple expands
GET /api/v3/store/products/prod_86Rf07xd4z?expand=brand,variants,categories
```

Response with `?expand=brand`:

```json theme={"theme":"night-owl"}
{
  "id": "prod_86Rf07xd4z",
  "name": "Air Max 90",
  "brand_id": "brand_k5nR8xLq",
  "brand": {
    "id": "brand_k5nR8xLq",
    "name": "Nike",
    "slug": "nike",
    "description": "Just Do It",
    "logo_url": "https://cdn.example.com/brands/nike-logo.png"
  }
}
```

## 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`:

```ruby app/serializers/my_app/product_serializer.rb theme={"theme":"night-owl"}
module MyApp
  class ProductSerializer < Spree::Api::V3::ProductSerializer
    attribute :my_field do |product|
      product.my_field
    end
  end
end
```

```ruby config/initializers/spree.rb theme={"theme":"night-owl"}
Spree::Api::Dependencies.product_serializer = 'MyApp::ProductSerializer'
```

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

```ruby app/models/spree/brand.rb theme={"theme":"night-owl"}
module Spree
  class Brand < Spree::Base
    include Spree::PrefixedId
    extend FriendlyId

    has_prefix_id :brand
    friendly_id :slug_candidates, use: [:slugged, :scoped], scope: spree_base_uniqueness_scope

    has_many :products, class_name: 'Spree::Product', dependent: :nullify

    has_one_attached :logo
    has_rich_text :description

    validates :name, presence: true
  end
end
```

### Brand Serializer

```ruby app/serializers/spree/api/v3/brand_serializer.rb theme={"theme":"night-owl"}
module Spree
  module Api
    module V3
      class BrandSerializer < BaseSerializer
        typelize name: :string,
                 slug: [:string, nullable: true],
                 description: [:string, nullable: true],
                 logo_url: [:string, nullable: true]

        attributes :name, :slug

        attribute :description do |brand|
          brand.description&.to_plain_text
        end

        attribute :logo_url do |brand|
          image_url_for(brand.logo) if brand.logo.attached?
        end
      end
    end
  end
end
```

### Brands Controller

```ruby app/controllers/spree/api/v3/store/brands_controller.rb theme={"theme":"night-owl"}
module Spree
  module Api
    module V3
      module Store
        class BrandsController < ResourceController
          protected

          def model_class
            Spree::Brand
          end

          def find_resource
            id = params[:id]
            if id.to_s.start_with?('brand_')
              scope.find_by_prefix_id!(id)
            else
              scope.friendly.find(id)
            end
          end

          def serializer_class
            Spree::Api::V3::BrandSerializer
          end

          def scope
            Spree::Brand.all
          end
        end
      end
    end
  end
end
```

### Custom Product Serializer

```ruby app/serializers/my_app/product_serializer.rb theme={"theme":"night-owl"}
module MyApp
  class ProductSerializer < Spree::Api::V3::ProductSerializer
    typelize brand_id: [:string, nullable: true]

    attribute :brand_id do |product|
      product.brand&.prefixed_id
    end

    one :brand,
        resource: Spree::Api::V3::BrandSerializer,
        if: proc { expand?('brand') }
  end
end
```

### Routes

```ruby config/routes.rb theme={"theme":"night-owl"}
Spree::Core::Engine.add_routes do
  namespace :api, defaults: { format: 'json' } do
    namespace :v3 do
      namespace :store do
        resources :brands, only: [:index, :show]
      end
    end
  end
end
```

### Initializer

```ruby config/initializers/spree.rb theme={"theme":"night-owl"}
# Permit brand_id in product params (from Extending Core Models tutorial)
Spree::PermittedAttributes.product_attributes << :brand_id

# Swap in custom product serializer with brand support
Spree::Api::Dependencies.product_serializer = 'MyApp::ProductSerializer'

# Allow filtering products by brand via Ransack
Spree.ransack.add_attribute(Spree::Product, :brand_id)
Spree.ransack.add_association(Spree::Product, :brand)
```

## Related Documentation

* [Extending Core Models](/developer/tutorial/extending-models) - Adding the Brand association to Product
* [Events & Webhooks](/developer/tutorial/events) - Lifecycle events for Brand and external-system integration (next step)
* [Using Brands with the SDK](/developer/tutorial/sdk) - Consuming brand endpoints from TypeScript
* [Decorators](/developer/customization/decorators) - Full decorator reference
* [Dependencies](/developer/customization/dependencies) - Swapping services and serializers
