spree:api_resource generates the model, its migration, a customer-facing Store API, a back-office Admin API, serializers, routes, a factory and controller specs in one command.
This matters for the rest of the tutorial. The dashboard plugin in step 2 talks to the Admin API, the storefront in step 3 talks to the Store API, and the events in step 4 reuse the serializer generated here. Getting this step right makes the other four mostly configuration.
Step 1: Generate the resource
A brand has a name, a slug for friendly URLs, and an active flag:Every command in this tutorial runs from the root of your create-spree-app project.
spree routes each one into the right container, so generators write into server/ and nothing needs installing on your machine.The field syntax
Attributes follow the familiarname:type:index form, with the Spree conventions applied for you:
Useful flags:
--paranoid for soft delete, --custom-fields for custom fields support, --writable to give the Store API create, update and destroy as well, and --no-store or --no-admin to generate only one side. Run the generator with --help for the full list.
Step 2: What you got
You do not need to read the generated code to use it. The command produced a complete, convention-correct API, and the contract it gives you is this:
Three conventions are worth knowing, because every later step depends on them:
- Public IDs, never database IDs. Records are addressed as
brand_k5nR8xLq. The API accepts and returns that form everywhere. - Two serializers, one inheriting the other. The Store serializer carries public fields only; the Admin serializer extends it and adds
created_atandupdated_at. Add a public field once and both surfaces get it. This is why customers never see back-office data by accident. - Filtering is allowlisted.
name,slugandactiveare queryable because the generator listed them. Anything not on that list is rejected, so a caller cannot filter by a column you did not intend to expose.
--writable if a resource genuinely needs customer writes, as carts and addresses do.
Brands belong to a store. Every resource you generate is scoped to one, because commerce data — catalog, configuration, orders — is always per-store. A new record picks up its store automatically, and a field marked unique is unique within a store, so two stores can each have a brand called wilson. Only genuinely global reference data, like countries, opts out with --no-store-scoped.
Step 3: Try it
Create a brand through the Admin API. You need a secret key from Settings → API keys in the dashboard:{ "brand": { ... } } wrapper around the attributes, and the response identifies the record as brand_k5nR8xLq rather than by a database ID.
Now read it back from the Store API, using a publishable key from the same settings page:
Connect brands to products
A brand on its own is a list. Adding abrand_id to products is what makes it useful in steps 2 and 3:
Spree::Product is the one place this step needs backend code — a decorator adds behavior to a core class without forking it. The generator writes the file:
prepended block:
server/app/models/spree/product_decorator.rb
app/models/spree/brand.rb, which is your own file:
optional: true lets a product exist without a brand. dependent: :nullify means deleting a brand clears the reference rather than deleting the products that carried it — almost always what you want for a label attached to a catalog.
Two more lines make the association usable from the API. The product serializer has to expose it, or the storefront cannot read a product’s brand:
server/app/serializers/spree/api/v3/product_serializer_decorator.rb
?q[brand_id_eq]=… is rejected:
server/app/models/spree/product_decorator.rb
brand_id on write, or the picker you build in step 2 will appear to work and save nothing. Extensions append to the model’s list rather than replacing it:
server/config/initializers/spree.rb
+=, never = — assigning would drop whatever another extension added.

