@spree/docs 0.1.176 → 0.1.178
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api-reference/store.yaml +48 -48
- package/dist/developer/cli/quickstart.md +1 -1
- package/dist/developer/contributing/creating-an-extension.md +2 -2
- package/dist/developer/core-concepts/addresses.md +3 -3
- package/dist/developer/core-concepts/architecture.md +4 -4
- package/dist/developer/core-concepts/calculators.md +2 -2
- package/dist/developer/core-concepts/carts.md +207 -0
- package/dist/developer/core-concepts/channels.md +3 -3
- package/dist/developer/core-concepts/customers.md +1 -1
- package/dist/developer/core-concepts/events.md +1 -1
- package/dist/developer/core-concepts/fulfillments.md +278 -0
- package/dist/developer/core-concepts/imports-exports.md +6 -18
- package/dist/developer/core-concepts/inventory.md +3 -3
- package/dist/developer/core-concepts/media.md +1 -1
- package/dist/developer/core-concepts/metafields.md +1 -1
- package/dist/developer/core-concepts/orders.md +92 -339
- package/dist/developer/core-concepts/payments.md +1 -1
- package/dist/developer/core-concepts/promotions.md +68 -195
- package/dist/developer/core-concepts/returns-exchanges-claims.md +173 -0
- package/dist/developer/core-concepts/store-credits-gift-cards.md +16 -18
- package/dist/developer/core-concepts/stores.md +1 -1
- package/dist/developer/core-concepts/taxes-discounts-fees.md +199 -0
- package/dist/developer/core-concepts/taxes.md +3 -3
- package/dist/developer/create-spree-app/quickstart.md +1 -1
- package/dist/developer/customization/api.md +36 -11
- package/dist/developer/customization/configuration.md +207 -37
- package/dist/developer/customization/decorators.md +8 -8
- package/dist/developer/customization/permissions.md +34 -229
- package/dist/developer/customization/quickstart.md +159 -108
- package/dist/developer/customization/validations.md +1 -1
- package/dist/developer/dashboard/overview.md +1 -1
- package/dist/developer/how-to/build-a-b2b-store.md +21 -0
- package/dist/developer/how-to/build-a-marketplace.md +25 -0
- package/dist/developer/how-to/custom-api-authentication.md +1 -1
- package/dist/developer/how-to/custom-delivery-rate-provider.md +242 -0
- package/dist/developer/how-to/custom-document-numbers.md +154 -0
- package/dist/developer/how-to/custom-order-routing.md +1 -1
- package/dist/developer/how-to/custom-payment-method.md +1 -1
- package/dist/developer/how-to/custom-promotion.md +76 -115
- package/dist/developer/how-to/custom-report.md +2 -2
- package/dist/developer/how-to/custom-stock-splitter.md +5 -5
- package/dist/developer/how-to/sell-digital-products.md +20 -0
- package/dist/developer/multi-tenant/quickstart.md +1 -1
- package/dist/developer/providers/dam.md +14 -0
- package/dist/developer/providers/erp.md +31 -0
- package/dist/developer/providers/fulfillment.md +25 -0
- package/dist/developer/{deployment/telemetry.md → providers/observability.md} +1 -1
- package/dist/developer/providers/overview.md +44 -0
- package/dist/developer/providers/pim.md +25 -0
- package/dist/developer/providers/sso.md +20 -0
- package/dist/developer/sdk/admin/extending.md +1 -1
- package/dist/developer/sdk/admin/quickstart.md +1 -1
- package/dist/developer/sdk/admin/resources.md +1 -1
- package/dist/developer/sdk/extending.md +1 -1
- package/dist/developer/tutorial/admin-api.md +13 -0
- package/dist/developer/tutorial/dashboard-plugin.md +14 -0
- package/dist/developer/tutorial/events.md +7 -161
- package/dist/developer/tutorial/introduction.md +15 -35
- package/dist/developer/tutorial/model.md +7 -98
- package/dist/developer/tutorial/store-api.md +13 -0
- package/dist/developer/tutorial/storefront.md +12 -0
- package/dist/developer/tutorial/testing.md +7 -711
- package/dist/developer/upgrades/5.6-to-6.0.md +2 -2
- package/package.json +1 -1
- package/dist/developer/admin/admin.md +0 -214
- package/dist/developer/admin/authentication.md +0 -59
- package/dist/developer/admin/components.md +0 -711
- package/dist/developer/admin/custom-css.md +0 -256
- package/dist/developer/admin/custom-javascript.md +0 -116
- package/dist/developer/admin/extending-ui.md +0 -1839
- package/dist/developer/admin/form-builder.md +0 -444
- package/dist/developer/admin/helper-methods.md +0 -531
- package/dist/developer/admin/navigation.md +0 -805
- package/dist/developer/admin/tables.md +0 -490
- package/dist/developer/advanced/adding_spree_to_rails_app.md +0 -92
- package/dist/developer/core-concepts/adjustments.md +0 -113
- package/dist/developer/core-concepts/reports.md +0 -208
- package/dist/developer/core-concepts/shipments.md +0 -307
- package/dist/developer/core-concepts/users.md +0 -303
- package/dist/developer/customization/authentication.md +0 -100
- package/dist/developer/customization/checkout.md +0 -202
- package/dist/developer/customization/emails.md +0 -18
- package/dist/developer/customization/routes.md +0 -24
- package/dist/developer/multi-vendor/installation.md +0 -61
- package/dist/developer/multi-vendor/quickstart.md +0 -17
- package/dist/developer/tutorial/admin.md +0 -206
- package/dist/developer/tutorial/api.md +0 -606
- package/dist/developer/tutorial/extending-models.md +0 -393
- package/dist/developer/tutorial/sdk.md +0 -170
- package/dist/developer/upgrades/2.0-to-2.1.md +0 -46
- package/dist/developer/upgrades/2.1-to-2.2.md +0 -59
- package/dist/developer/upgrades/2.2-to-2.3.md +0 -44
- package/dist/developer/upgrades/2.3-to-2.4.md +0 -42
- package/dist/developer/upgrades/3.0-to-3.1.md +0 -47
- package/dist/developer/upgrades/3.1-to-3.2.md +0 -34
- package/dist/developer/upgrades/3.2-to-3.3.md +0 -70
- package/dist/developer/upgrades/3.3-to-3.4.md +0 -36
- package/dist/developer/upgrades/3.4-to-3.5.md +0 -44
- package/dist/developer/upgrades/3.5-to-3.6.md +0 -40
- package/dist/developer/upgrades/3.6-to-3.7.md +0 -62
- package/dist/developer/upgrades/3.7-to-4.0.md +0 -152
- package/dist/developer/upgrades/4.0-to-4.1.md +0 -92
- package/dist/developer/upgrades/4.1-to-4.2.md +0 -109
- package/dist/developer/upgrades/4.10-to-5.0.md +0 -131
- package/dist/developer/upgrades/4.2-to-4.3.md +0 -100
- package/dist/developer/upgrades/4.3-to-4.4.md +0 -125
- package/dist/developer/upgrades/4.4-to-4.5.md +0 -94
- package/dist/developer/upgrades/4.5-to-4.6.md +0 -119
- package/dist/developer/upgrades/4.6-to-4.7.md +0 -39
- package/dist/developer/upgrades/4.8-to-4.9.md +0 -24
- package/dist/developer/upgrades/4.9-to-4.10.md +0 -24
- package/dist/developer/upgrades/4.x-to-4.8.md +0 -52
- package/dist/developer/upgrades/5.0-to-5.1.md +0 -28
- package/dist/developer/upgrades/5.1-to-5.2.md +0 -131
- package/dist/developer/upgrades/5.2-to-5.3.md +0 -338
- package/dist/developer/upgrades/5.3-to-5.4.md +0 -277
- package/dist/developer/upgrades/5.4-to-5.5.md +0 -301
- package/dist/developer/upgrades/5.5-to-5.6.md +0 -207
|
@@ -1,606 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Expose a custom model through the Store and Admin APIs
|
|
3
|
-
sidebarTitle: API
|
|
4
|
-
description: Expose your custom Brand model through the Spree Store and Admin REST APIs with serializers, controllers, routes, and the spree:api_resource generator.
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
> **NOTE:** 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.
|
|
8
|
-
|
|
9
|
-
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.
|
|
10
|
-
|
|
11
|
-
> **INFO:** This guide assumes you've completed the [Model](model.md), [Admin](admin.md), and [Extending Core Models](extending-models.md) tutorials.
|
|
12
|
-
|
|
13
|
-
## What We're Building
|
|
14
|
-
|
|
15
|
-
By the end of this tutorial, you'll have:
|
|
16
|
-
|
|
17
|
-
- `GET /api/v3/store/brands` and `GET /api/v3/store/brands/:id` — customer-facing, read-only, lookup by prefixed ID or slug
|
|
18
|
-
- Full CRUD on `/api/v3/admin/brands` — for back-office apps and integrations
|
|
19
|
-
- Brand data included in Product responses via `?expand=brand`
|
|
20
|
-
- Understanding of how to add new API endpoints and extend existing serializers
|
|
21
|
-
|
|
22
|
-
## The Fast Path: One Generator Command
|
|
23
|
-
|
|
24
|
-
Everything this page builds by hand can be generated in one command with `spree:api_resource`:
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
```bash Spree CLI (Docker)
|
|
28
|
-
spree generate api_resource Brand
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
```bash Without Spree CLI
|
|
32
|
-
bin/rails g spree:api_resource Brand
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
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:
|
|
37
|
-
|
|
38
|
-
```text
|
|
39
|
-
skip model app/models/spree/brand.rb (owned-once; already exists)
|
|
40
|
-
skip migration (model already exists; add a new migration for schema changes)
|
|
41
|
-
create app/controllers/spree/api/v3/store/brands_controller.rb
|
|
42
|
-
create app/controllers/spree/api/v3/admin/brands_controller.rb
|
|
43
|
-
create app/serializers/spree/api/v3/brand_serializer.rb
|
|
44
|
-
create app/serializers/spree/api/v3/admin/brand_serializer.rb
|
|
45
|
-
create spec/factories/spree/brand_factory.rb
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
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.
|
|
49
|
-
|
|
50
|
-
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.
|
|
51
|
-
|
|
52
|
-
> **TIP:** Using an AI agent? The [Spree agent skills](../agentic/agent-skills.md) include a dedicated resource-generator skill — your agent knows the field syntax, the flags, and the generated-file contract.
|
|
53
|
-
|
|
54
|
-
## How the Store API Works
|
|
55
|
-
|
|
56
|
-
Every Store API endpoint follows the same pattern:
|
|
57
|
-
|
|
58
|
-
1. **Controller** inherits from `Spree::Api::V3::Store::ResourceController` which provides CRUD, pagination, Ransack filtering, and authorization out of the box
|
|
59
|
-
2. **Serializer** inherits from `Spree::Api::V3::BaseSerializer` (uses [Alba](https://github.com/okuramasafumi/alba)) and defines which fields to return
|
|
60
|
-
3. **Routes** are added via `Spree::Core::Engine.add_routes`
|
|
61
|
-
4. **Serializer registration** via `Spree::Api::Dependencies` enables dependency injection so serializers can be swapped by extensions or the host app
|
|
62
|
-
|
|
63
|
-
## Step 1: Prepare the Brand Model for the API
|
|
64
|
-
|
|
65
|
-
Store API requires two things from models:
|
|
66
|
-
|
|
67
|
-
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](model.md), so this is done.
|
|
68
|
-
2. **Slugs** — human-readable URL identifiers like `nike` for `GET /brands/nike`
|
|
69
|
-
|
|
70
|
-
Add a `slug` column:
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
```bash Spree CLI (Docker)
|
|
74
|
-
spree generate migration AddSlugToSpreeBrands slug:string:uniq
|
|
75
|
-
spree migrate
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
```bash Without Spree CLI
|
|
79
|
-
bin/rails g migration AddSlugToSpreeBrands slug:string:uniq
|
|
80
|
-
bin/rails db:migrate
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
Then add `FriendlyId` to the Brand model:
|
|
85
|
-
|
|
86
|
-
```ruby app/models/spree/brand.rb {3,6}
|
|
87
|
-
module Spree
|
|
88
|
-
class Brand < Spree.base_class
|
|
89
|
-
extend FriendlyId
|
|
90
|
-
|
|
91
|
-
has_prefix_id :brand
|
|
92
|
-
friendly_id :slug_candidates, use: [:slugged, :scoped], scope: spree_base_uniqueness_scope
|
|
93
|
-
|
|
94
|
-
has_many :products, class_name: 'Spree::Product', dependent: :nullify
|
|
95
|
-
|
|
96
|
-
has_rich_text :description
|
|
97
|
-
has_one_attached :logo
|
|
98
|
-
|
|
99
|
-
validates :name, presence: true
|
|
100
|
-
|
|
101
|
-
self.whitelisted_ransackable_attributes = %w[name]
|
|
102
|
-
self.whitelisted_ransackable_associations = %w[]
|
|
103
|
-
self.whitelisted_ransackable_scopes = %w[]
|
|
104
|
-
end
|
|
105
|
-
end
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
Now:
|
|
109
|
-
- `Spree::Brand.first.prefixed_id` returns `brand_k5nR8xLq`
|
|
110
|
-
- `Spree::Brand.find_by_prefix_id!('brand_k5nR8xLq')` finds by prefixed ID
|
|
111
|
-
- `Spree::Brand.friendly.find('nike')` finds by slug
|
|
112
|
-
- Slugs are auto-generated from the `name` via `slug_candidates` (inherited from the Spree base class)
|
|
113
|
-
|
|
114
|
-
## Step 2: Create the Serializer
|
|
115
|
-
|
|
116
|
-
Create a serializer that defines the JSON response shape for brands:
|
|
117
|
-
|
|
118
|
-
```ruby app/serializers/spree/api/v3/brand_serializer.rb
|
|
119
|
-
module Spree
|
|
120
|
-
module Api
|
|
121
|
-
module V3
|
|
122
|
-
class BrandSerializer < BaseSerializer
|
|
123
|
-
typelize name: :string,
|
|
124
|
-
slug: [:string, nullable: true],
|
|
125
|
-
description: [:string, nullable: true],
|
|
126
|
-
logo_url: [:string, nullable: true]
|
|
127
|
-
|
|
128
|
-
attributes :name, :slug
|
|
129
|
-
|
|
130
|
-
attribute :description do |brand|
|
|
131
|
-
brand.description&.to_plain_text
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
attribute :logo_url do |brand|
|
|
135
|
-
image_url_for(brand.logo) if brand.logo.attached?
|
|
136
|
-
end
|
|
137
|
-
end
|
|
138
|
-
end
|
|
139
|
-
end
|
|
140
|
-
end
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
### Understanding the Serializer
|
|
144
|
-
|
|
145
|
-
- **`BaseSerializer`** automatically converts `id` to a prefixed ID and provides context helpers (`current_store`, `current_currency`, etc.)
|
|
146
|
-
- **`typelize`** provides type hints used by [Typelizer](https://github.com/skryukov/typelizer) to auto-generate TypeScript types for the SDK
|
|
147
|
-
- **`attributes`** lists database columns to include directly
|
|
148
|
-
- **`attribute ... do`** blocks define computed fields (like stripping HTML from rich text, or generating image URLs)
|
|
149
|
-
|
|
150
|
-
## Step 3: Create the Controller
|
|
151
|
-
|
|
152
|
-
Create a controller that inherits from `Store::ResourceController`:
|
|
153
|
-
|
|
154
|
-
```ruby app/controllers/spree/api/v3/store/brands_controller.rb
|
|
155
|
-
module Spree
|
|
156
|
-
module Api
|
|
157
|
-
module V3
|
|
158
|
-
module Store
|
|
159
|
-
class BrandsController < ResourceController
|
|
160
|
-
protected
|
|
161
|
-
|
|
162
|
-
def model_class
|
|
163
|
-
Spree::Brand
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
def serializer_class
|
|
167
|
-
Spree::Api::V3::BrandSerializer
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
def scope
|
|
171
|
-
Spree::Brand.all
|
|
172
|
-
end
|
|
173
|
-
end
|
|
174
|
-
end
|
|
175
|
-
end
|
|
176
|
-
end
|
|
177
|
-
end
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
### Understanding the Controller
|
|
181
|
-
|
|
182
|
-
`ResourceController` gives you `index` and `show` actions automatically. You only need to define:
|
|
183
|
-
|
|
184
|
-
| Method | Purpose |
|
|
185
|
-
|--------|---------|
|
|
186
|
-
| `model_class` | Which ActiveRecord model to query |
|
|
187
|
-
| `serializer_class` | Which serializer to render responses with |
|
|
188
|
-
| `scope` | Base query scope (add `.where(...)` to filter) |
|
|
189
|
-
|
|
190
|
-
The base controller handles:
|
|
191
|
-
- **Pagination** via [Pagy](https://github.com/ddnexus/pagy) (`?page=2&limit=25`)
|
|
192
|
-
- **Filtering** via [Ransack](https://github.com/activerecord-hackery/ransack) (`?q[name_cont]=nike`)
|
|
193
|
-
- **Sorting** via JSON:API style (`?sort=-name` for descending)
|
|
194
|
-
- **Authorization** via [CanCanCan](https://github.com/CanCanCommunity/cancancan)
|
|
195
|
-
- **Prefixed ID lookup** for `show` action (`/brands/brand_k5nR8xLq`)
|
|
196
|
-
|
|
197
|
-
> **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.
|
|
198
|
-
|
|
199
|
-
### Adding Slug Lookup
|
|
200
|
-
|
|
201
|
-
To also support fetching brands by slug (like products support `/products/blue-t-shirt`), override `find_resource`:
|
|
202
|
-
|
|
203
|
-
```ruby app/controllers/spree/api/v3/store/brands_controller.rb {12-19}
|
|
204
|
-
module Spree
|
|
205
|
-
module Api
|
|
206
|
-
module V3
|
|
207
|
-
module Store
|
|
208
|
-
class BrandsController < ResourceController
|
|
209
|
-
protected
|
|
210
|
-
|
|
211
|
-
def model_class
|
|
212
|
-
Spree::Brand
|
|
213
|
-
end
|
|
214
|
-
|
|
215
|
-
def find_resource
|
|
216
|
-
id = params[:id]
|
|
217
|
-
if id.to_s.start_with?('brand_')
|
|
218
|
-
scope.find_by_prefix_id!(id)
|
|
219
|
-
else
|
|
220
|
-
scope.friendly.find(id)
|
|
221
|
-
end
|
|
222
|
-
end
|
|
223
|
-
|
|
224
|
-
def serializer_class
|
|
225
|
-
Spree::Api::V3::BrandSerializer
|
|
226
|
-
end
|
|
227
|
-
|
|
228
|
-
def scope
|
|
229
|
-
Spree::Brand.all
|
|
230
|
-
end
|
|
231
|
-
end
|
|
232
|
-
end
|
|
233
|
-
end
|
|
234
|
-
end
|
|
235
|
-
end
|
|
236
|
-
```
|
|
237
|
-
|
|
238
|
-
## Step 4: Add Routes
|
|
239
|
-
|
|
240
|
-
Add the routes for your new endpoints:
|
|
241
|
-
|
|
242
|
-
```ruby config/routes.rb
|
|
243
|
-
Spree::Core::Engine.add_routes do
|
|
244
|
-
namespace :api, defaults: { format: 'json' } do
|
|
245
|
-
namespace :v3 do
|
|
246
|
-
namespace :store do
|
|
247
|
-
resources :brands, only: [:index, :show]
|
|
248
|
-
end
|
|
249
|
-
end
|
|
250
|
-
end
|
|
251
|
-
end
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
This creates:
|
|
255
|
-
- `GET /api/v3/store/brands` — paginated list with filtering/sorting
|
|
256
|
-
- `GET /api/v3/store/brands/:id` — single brand by prefixed ID or slug
|
|
257
|
-
|
|
258
|
-
## Step 5: Test the Endpoints
|
|
259
|
-
|
|
260
|
-
Restart your server and test:
|
|
261
|
-
|
|
262
|
-
```bash
|
|
263
|
-
# List brands
|
|
264
|
-
curl -H "X-Spree-API-Key: pk_YOUR_KEY" \
|
|
265
|
-
http://localhost:3000/api/v3/store/brands
|
|
266
|
-
|
|
267
|
-
# Get a single brand
|
|
268
|
-
curl -H "X-Spree-API-Key: pk_YOUR_KEY" \
|
|
269
|
-
http://localhost:3000/api/v3/store/brands/brand_k5nR8xLq
|
|
270
|
-
|
|
271
|
-
# Filter by name
|
|
272
|
-
curl -H "X-Spree-API-Key: pk_YOUR_KEY" \
|
|
273
|
-
"http://localhost:3000/api/v3/store/brands?q[name_cont]=nike"
|
|
274
|
-
|
|
275
|
-
# Sort alphabetically
|
|
276
|
-
curl -H "X-Spree-API-Key: pk_YOUR_KEY" \
|
|
277
|
-
"http://localhost:3000/api/v3/store/brands?sort=name"
|
|
278
|
-
```
|
|
279
|
-
|
|
280
|
-
### Response Format
|
|
281
|
-
|
|
282
|
-
List response:
|
|
283
|
-
|
|
284
|
-
```json
|
|
285
|
-
{
|
|
286
|
-
"data": [
|
|
287
|
-
{
|
|
288
|
-
"id": "brand_k5nR8xLq",
|
|
289
|
-
"name": "Nike",
|
|
290
|
-
"slug": "nike",
|
|
291
|
-
"description": "Just Do It",
|
|
292
|
-
"logo_url": "https://cdn.example.com/brands/nike-logo.png"
|
|
293
|
-
}
|
|
294
|
-
],
|
|
295
|
-
"meta": {
|
|
296
|
-
"page": 1,
|
|
297
|
-
"limit": 25,
|
|
298
|
-
"count": 42,
|
|
299
|
-
"pages": 2,
|
|
300
|
-
"from": 1,
|
|
301
|
-
"to": 25,
|
|
302
|
-
"in": 25,
|
|
303
|
-
"previous": null,
|
|
304
|
-
"next": 2
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
```
|
|
308
|
-
|
|
309
|
-
## The Admin API
|
|
310
|
-
|
|
311
|
-
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:
|
|
312
|
-
|
|
313
|
-
```ruby app/serializers/spree/api/v3/admin/brand_serializer.rb
|
|
314
|
-
module Spree
|
|
315
|
-
module Api
|
|
316
|
-
module V3
|
|
317
|
-
module Admin
|
|
318
|
-
class BrandSerializer < V3::BrandSerializer
|
|
319
|
-
attributes :created_at, :updated_at
|
|
320
|
-
end
|
|
321
|
-
end
|
|
322
|
-
end
|
|
323
|
-
end
|
|
324
|
-
end
|
|
325
|
-
```
|
|
326
|
-
|
|
327
|
-
```ruby app/controllers/spree/api/v3/admin/brands_controller.rb
|
|
328
|
-
module Spree
|
|
329
|
-
module Api
|
|
330
|
-
module V3
|
|
331
|
-
module Admin
|
|
332
|
-
class BrandsController < ResourceController
|
|
333
|
-
protected
|
|
334
|
-
|
|
335
|
-
def model_class
|
|
336
|
-
Spree::Brand
|
|
337
|
-
end
|
|
338
|
-
|
|
339
|
-
def serializer_class
|
|
340
|
-
Spree::Api::V3::Admin::BrandSerializer
|
|
341
|
-
end
|
|
342
|
-
|
|
343
|
-
def permitted_params
|
|
344
|
-
params.permit(:name)
|
|
345
|
-
end
|
|
346
|
-
end
|
|
347
|
-
end
|
|
348
|
-
end
|
|
349
|
-
end
|
|
350
|
-
end
|
|
351
|
-
```
|
|
352
|
-
|
|
353
|
-
Two conventions to notice:
|
|
354
|
-
|
|
355
|
-
- **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.
|
|
356
|
-
- **`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).
|
|
357
|
-
|
|
358
|
-
With the routes registered (`resources :brands` under the `admin` namespace — the generator injects this), back-office clients get:
|
|
359
|
-
|
|
360
|
-
```bash
|
|
361
|
-
# Create a brand with a secret API key
|
|
362
|
-
curl -X POST -H "X-Spree-API-Key: sk_YOUR_KEY" \
|
|
363
|
-
-H "Content-Type: application/json" \
|
|
364
|
-
-d '{"name": "Adidas"}' \
|
|
365
|
-
http://localhost:3000/api/v3/admin/brands
|
|
366
|
-
```
|
|
367
|
-
|
|
368
|
-
> **INFO:** Secret keys carry scopes (`read_brands`, `write_brands` style) and JWT admin users go through CanCanCan abilities — see [API authentication](../customization/api.md) for the full model. From TypeScript, the [Admin SDK](../sdk/admin/quickstart.md) wraps the Admin API with typed clients for all built-in resources.
|
|
369
|
-
|
|
370
|
-
## Step 6: Add Brand to Product Responses
|
|
371
|
-
|
|
372
|
-
Now let's extend the Product serializer so that brand data is included when a storefront requests `?expand=brand`.
|
|
373
|
-
|
|
374
|
-
### Create a Custom Product Serializer
|
|
375
|
-
|
|
376
|
-
Subclass the core `ProductSerializer` and add brand fields. Then swap it in via Dependencies:
|
|
377
|
-
|
|
378
|
-
```ruby app/serializers/my_app/product_serializer.rb
|
|
379
|
-
module MyApp
|
|
380
|
-
class ProductSerializer < Spree::Api::V3::ProductSerializer
|
|
381
|
-
typelize brand_id: [:string, nullable: true]
|
|
382
|
-
|
|
383
|
-
attribute :brand_id do |product|
|
|
384
|
-
product.brand&.prefixed_id
|
|
385
|
-
end
|
|
386
|
-
|
|
387
|
-
one :brand,
|
|
388
|
-
resource: Spree::Api::V3::BrandSerializer,
|
|
389
|
-
if: proc { expand?('brand') }
|
|
390
|
-
end
|
|
391
|
-
end
|
|
392
|
-
```
|
|
393
|
-
|
|
394
|
-
Register it and whitelist the brand association for Ransack filtering in your initializer:
|
|
395
|
-
|
|
396
|
-
```ruby config/initializers/spree.rb
|
|
397
|
-
# Swap in custom product serializer with brand support
|
|
398
|
-
Spree::Api::Dependencies.product_serializer = 'MyApp::ProductSerializer'
|
|
399
|
-
|
|
400
|
-
# Allow filtering products by brand (e.g., ?q[brand_name_cont]=nike or ?q[brand_id_eq]=123)
|
|
401
|
-
Spree.ransack.add_attribute(Spree::Product, :brand_id)
|
|
402
|
-
Spree.ransack.add_association(Spree::Product, :brand)
|
|
403
|
-
```
|
|
404
|
-
|
|
405
|
-
> **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.
|
|
406
|
-
|
|
407
|
-
### Understanding the Serializer
|
|
408
|
-
|
|
409
|
-
- **`brand_id`** — always included as a flat attribute (prefixed ID string), so storefronts know which brand a product belongs to without expanding
|
|
410
|
-
- **`one :brand`** — conditionally included when the client requests `?expand=brand`, returns the full brand object inline
|
|
411
|
-
- **`expand?('brand')`** — checks if the `expand` query parameter includes `'brand'`
|
|
412
|
-
|
|
413
|
-
> **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.
|
|
414
|
-
|
|
415
|
-
### How Expand Works
|
|
416
|
-
|
|
417
|
-
The expand system keeps responses lean by default and lets clients opt-in to nested data:
|
|
418
|
-
|
|
419
|
-
```bash
|
|
420
|
-
# Without expand — brand_id only
|
|
421
|
-
GET /api/v3/store/products/prod_86Rf07xd4z
|
|
422
|
-
|
|
423
|
-
# With expand — full brand object included
|
|
424
|
-
GET /api/v3/store/products/prod_86Rf07xd4z?expand=brand
|
|
425
|
-
|
|
426
|
-
# Multiple expands
|
|
427
|
-
GET /api/v3/store/products/prod_86Rf07xd4z?expand=brand,variants,categories
|
|
428
|
-
```
|
|
429
|
-
|
|
430
|
-
Response with `?expand=brand`:
|
|
431
|
-
|
|
432
|
-
```json
|
|
433
|
-
{
|
|
434
|
-
"id": "prod_86Rf07xd4z",
|
|
435
|
-
"name": "Air Max 90",
|
|
436
|
-
"brand_id": "brand_k5nR8xLq",
|
|
437
|
-
"brand": {
|
|
438
|
-
"id": "brand_k5nR8xLq",
|
|
439
|
-
"name": "Nike",
|
|
440
|
-
"slug": "nike",
|
|
441
|
-
"description": "Just Do It",
|
|
442
|
-
"logo_url": "https://cdn.example.com/brands/nike-logo.png"
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
```
|
|
446
|
-
|
|
447
|
-
## Extending Core Serializers (General Pattern)
|
|
448
|
-
|
|
449
|
-
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`:
|
|
450
|
-
|
|
451
|
-
```ruby app/serializers/my_app/product_serializer.rb
|
|
452
|
-
module MyApp
|
|
453
|
-
class ProductSerializer < Spree::Api::V3::ProductSerializer
|
|
454
|
-
attribute :my_field do |product|
|
|
455
|
-
product.my_field
|
|
456
|
-
end
|
|
457
|
-
end
|
|
458
|
-
end
|
|
459
|
-
```
|
|
460
|
-
|
|
461
|
-
```ruby config/initializers/spree.rb
|
|
462
|
-
Spree::Api::Dependencies.product_serializer = 'MyApp::ProductSerializer'
|
|
463
|
-
```
|
|
464
|
-
|
|
465
|
-
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.
|
|
466
|
-
|
|
467
|
-
## Complete Files
|
|
468
|
-
|
|
469
|
-
### Brand Model
|
|
470
|
-
|
|
471
|
-
```ruby app/models/spree/brand.rb
|
|
472
|
-
module Spree
|
|
473
|
-
class Brand < Spree::Base
|
|
474
|
-
include Spree::PrefixedId
|
|
475
|
-
extend FriendlyId
|
|
476
|
-
|
|
477
|
-
has_prefix_id :brand
|
|
478
|
-
friendly_id :slug_candidates, use: [:slugged, :scoped], scope: spree_base_uniqueness_scope
|
|
479
|
-
|
|
480
|
-
has_many :products, class_name: 'Spree::Product', dependent: :nullify
|
|
481
|
-
|
|
482
|
-
has_one_attached :logo
|
|
483
|
-
has_rich_text :description
|
|
484
|
-
|
|
485
|
-
validates :name, presence: true
|
|
486
|
-
end
|
|
487
|
-
end
|
|
488
|
-
```
|
|
489
|
-
|
|
490
|
-
### Brand Serializer
|
|
491
|
-
|
|
492
|
-
```ruby app/serializers/spree/api/v3/brand_serializer.rb
|
|
493
|
-
module Spree
|
|
494
|
-
module Api
|
|
495
|
-
module V3
|
|
496
|
-
class BrandSerializer < BaseSerializer
|
|
497
|
-
typelize name: :string,
|
|
498
|
-
slug: [:string, nullable: true],
|
|
499
|
-
description: [:string, nullable: true],
|
|
500
|
-
logo_url: [:string, nullable: true]
|
|
501
|
-
|
|
502
|
-
attributes :name, :slug
|
|
503
|
-
|
|
504
|
-
attribute :description do |brand|
|
|
505
|
-
brand.description&.to_plain_text
|
|
506
|
-
end
|
|
507
|
-
|
|
508
|
-
attribute :logo_url do |brand|
|
|
509
|
-
image_url_for(brand.logo) if brand.logo.attached?
|
|
510
|
-
end
|
|
511
|
-
end
|
|
512
|
-
end
|
|
513
|
-
end
|
|
514
|
-
end
|
|
515
|
-
```
|
|
516
|
-
|
|
517
|
-
### Brands Controller
|
|
518
|
-
|
|
519
|
-
```ruby app/controllers/spree/api/v3/store/brands_controller.rb
|
|
520
|
-
module Spree
|
|
521
|
-
module Api
|
|
522
|
-
module V3
|
|
523
|
-
module Store
|
|
524
|
-
class BrandsController < ResourceController
|
|
525
|
-
protected
|
|
526
|
-
|
|
527
|
-
def model_class
|
|
528
|
-
Spree::Brand
|
|
529
|
-
end
|
|
530
|
-
|
|
531
|
-
def find_resource
|
|
532
|
-
id = params[:id]
|
|
533
|
-
if id.to_s.start_with?('brand_')
|
|
534
|
-
scope.find_by_prefix_id!(id)
|
|
535
|
-
else
|
|
536
|
-
scope.friendly.find(id)
|
|
537
|
-
end
|
|
538
|
-
end
|
|
539
|
-
|
|
540
|
-
def serializer_class
|
|
541
|
-
Spree::Api::V3::BrandSerializer
|
|
542
|
-
end
|
|
543
|
-
|
|
544
|
-
def scope
|
|
545
|
-
Spree::Brand.all
|
|
546
|
-
end
|
|
547
|
-
end
|
|
548
|
-
end
|
|
549
|
-
end
|
|
550
|
-
end
|
|
551
|
-
end
|
|
552
|
-
```
|
|
553
|
-
|
|
554
|
-
### Custom Product Serializer
|
|
555
|
-
|
|
556
|
-
```ruby app/serializers/my_app/product_serializer.rb
|
|
557
|
-
module MyApp
|
|
558
|
-
class ProductSerializer < Spree::Api::V3::ProductSerializer
|
|
559
|
-
typelize brand_id: [:string, nullable: true]
|
|
560
|
-
|
|
561
|
-
attribute :brand_id do |product|
|
|
562
|
-
product.brand&.prefixed_id
|
|
563
|
-
end
|
|
564
|
-
|
|
565
|
-
one :brand,
|
|
566
|
-
resource: Spree::Api::V3::BrandSerializer,
|
|
567
|
-
if: proc { expand?('brand') }
|
|
568
|
-
end
|
|
569
|
-
end
|
|
570
|
-
```
|
|
571
|
-
|
|
572
|
-
### Routes
|
|
573
|
-
|
|
574
|
-
```ruby config/routes.rb
|
|
575
|
-
Spree::Core::Engine.add_routes do
|
|
576
|
-
namespace :api, defaults: { format: 'json' } do
|
|
577
|
-
namespace :v3 do
|
|
578
|
-
namespace :store do
|
|
579
|
-
resources :brands, only: [:index, :show]
|
|
580
|
-
end
|
|
581
|
-
end
|
|
582
|
-
end
|
|
583
|
-
end
|
|
584
|
-
```
|
|
585
|
-
|
|
586
|
-
### Initializer
|
|
587
|
-
|
|
588
|
-
```ruby config/initializers/spree.rb
|
|
589
|
-
# Permit brand_id in product params (from Extending Core Models tutorial)
|
|
590
|
-
Spree::PermittedAttributes.product_attributes << :brand_id
|
|
591
|
-
|
|
592
|
-
# Swap in custom product serializer with brand support
|
|
593
|
-
Spree::Api::Dependencies.product_serializer = 'MyApp::ProductSerializer'
|
|
594
|
-
|
|
595
|
-
# Allow filtering products by brand via Ransack
|
|
596
|
-
Spree.ransack.add_attribute(Spree::Product, :brand_id)
|
|
597
|
-
Spree.ransack.add_association(Spree::Product, :brand)
|
|
598
|
-
```
|
|
599
|
-
|
|
600
|
-
## Related Documentation
|
|
601
|
-
|
|
602
|
-
- [Extending Core Models](extending-models.md) - Adding the Brand association to Product
|
|
603
|
-
- [Events & Webhooks](events.md) - Lifecycle events for Brand and external-system integration (next step)
|
|
604
|
-
- [Using Brands with the SDK](sdk.md) - Consuming brand endpoints from TypeScript
|
|
605
|
-
- [Decorators](../customization/decorators.md) - Full decorator reference
|
|
606
|
-
- [Dependencies](../customization/dependencies.md) - Swapping services and serializers
|