@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.
Files changed (118) hide show
  1. package/dist/api-reference/store.yaml +48 -48
  2. package/dist/developer/cli/quickstart.md +1 -1
  3. package/dist/developer/contributing/creating-an-extension.md +2 -2
  4. package/dist/developer/core-concepts/addresses.md +3 -3
  5. package/dist/developer/core-concepts/architecture.md +4 -4
  6. package/dist/developer/core-concepts/calculators.md +2 -2
  7. package/dist/developer/core-concepts/carts.md +207 -0
  8. package/dist/developer/core-concepts/channels.md +3 -3
  9. package/dist/developer/core-concepts/customers.md +1 -1
  10. package/dist/developer/core-concepts/events.md +1 -1
  11. package/dist/developer/core-concepts/fulfillments.md +278 -0
  12. package/dist/developer/core-concepts/imports-exports.md +6 -18
  13. package/dist/developer/core-concepts/inventory.md +3 -3
  14. package/dist/developer/core-concepts/media.md +1 -1
  15. package/dist/developer/core-concepts/metafields.md +1 -1
  16. package/dist/developer/core-concepts/orders.md +92 -339
  17. package/dist/developer/core-concepts/payments.md +1 -1
  18. package/dist/developer/core-concepts/promotions.md +68 -195
  19. package/dist/developer/core-concepts/returns-exchanges-claims.md +173 -0
  20. package/dist/developer/core-concepts/store-credits-gift-cards.md +16 -18
  21. package/dist/developer/core-concepts/stores.md +1 -1
  22. package/dist/developer/core-concepts/taxes-discounts-fees.md +199 -0
  23. package/dist/developer/core-concepts/taxes.md +3 -3
  24. package/dist/developer/create-spree-app/quickstart.md +1 -1
  25. package/dist/developer/customization/api.md +36 -11
  26. package/dist/developer/customization/configuration.md +207 -37
  27. package/dist/developer/customization/decorators.md +8 -8
  28. package/dist/developer/customization/permissions.md +34 -229
  29. package/dist/developer/customization/quickstart.md +159 -108
  30. package/dist/developer/customization/validations.md +1 -1
  31. package/dist/developer/dashboard/overview.md +1 -1
  32. package/dist/developer/how-to/build-a-b2b-store.md +21 -0
  33. package/dist/developer/how-to/build-a-marketplace.md +25 -0
  34. package/dist/developer/how-to/custom-api-authentication.md +1 -1
  35. package/dist/developer/how-to/custom-delivery-rate-provider.md +242 -0
  36. package/dist/developer/how-to/custom-document-numbers.md +154 -0
  37. package/dist/developer/how-to/custom-order-routing.md +1 -1
  38. package/dist/developer/how-to/custom-payment-method.md +1 -1
  39. package/dist/developer/how-to/custom-promotion.md +76 -115
  40. package/dist/developer/how-to/custom-report.md +2 -2
  41. package/dist/developer/how-to/custom-stock-splitter.md +5 -5
  42. package/dist/developer/how-to/sell-digital-products.md +20 -0
  43. package/dist/developer/multi-tenant/quickstart.md +1 -1
  44. package/dist/developer/providers/dam.md +14 -0
  45. package/dist/developer/providers/erp.md +31 -0
  46. package/dist/developer/providers/fulfillment.md +25 -0
  47. package/dist/developer/{deployment/telemetry.md → providers/observability.md} +1 -1
  48. package/dist/developer/providers/overview.md +44 -0
  49. package/dist/developer/providers/pim.md +25 -0
  50. package/dist/developer/providers/sso.md +20 -0
  51. package/dist/developer/sdk/admin/extending.md +1 -1
  52. package/dist/developer/sdk/admin/quickstart.md +1 -1
  53. package/dist/developer/sdk/admin/resources.md +1 -1
  54. package/dist/developer/sdk/extending.md +1 -1
  55. package/dist/developer/tutorial/admin-api.md +13 -0
  56. package/dist/developer/tutorial/dashboard-plugin.md +14 -0
  57. package/dist/developer/tutorial/events.md +7 -161
  58. package/dist/developer/tutorial/introduction.md +15 -35
  59. package/dist/developer/tutorial/model.md +7 -98
  60. package/dist/developer/tutorial/store-api.md +13 -0
  61. package/dist/developer/tutorial/storefront.md +12 -0
  62. package/dist/developer/tutorial/testing.md +7 -711
  63. package/dist/developer/upgrades/5.6-to-6.0.md +2 -2
  64. package/package.json +1 -1
  65. package/dist/developer/admin/admin.md +0 -214
  66. package/dist/developer/admin/authentication.md +0 -59
  67. package/dist/developer/admin/components.md +0 -711
  68. package/dist/developer/admin/custom-css.md +0 -256
  69. package/dist/developer/admin/custom-javascript.md +0 -116
  70. package/dist/developer/admin/extending-ui.md +0 -1839
  71. package/dist/developer/admin/form-builder.md +0 -444
  72. package/dist/developer/admin/helper-methods.md +0 -531
  73. package/dist/developer/admin/navigation.md +0 -805
  74. package/dist/developer/admin/tables.md +0 -490
  75. package/dist/developer/advanced/adding_spree_to_rails_app.md +0 -92
  76. package/dist/developer/core-concepts/adjustments.md +0 -113
  77. package/dist/developer/core-concepts/reports.md +0 -208
  78. package/dist/developer/core-concepts/shipments.md +0 -307
  79. package/dist/developer/core-concepts/users.md +0 -303
  80. package/dist/developer/customization/authentication.md +0 -100
  81. package/dist/developer/customization/checkout.md +0 -202
  82. package/dist/developer/customization/emails.md +0 -18
  83. package/dist/developer/customization/routes.md +0 -24
  84. package/dist/developer/multi-vendor/installation.md +0 -61
  85. package/dist/developer/multi-vendor/quickstart.md +0 -17
  86. package/dist/developer/tutorial/admin.md +0 -206
  87. package/dist/developer/tutorial/api.md +0 -606
  88. package/dist/developer/tutorial/extending-models.md +0 -393
  89. package/dist/developer/tutorial/sdk.md +0 -170
  90. package/dist/developer/upgrades/2.0-to-2.1.md +0 -46
  91. package/dist/developer/upgrades/2.1-to-2.2.md +0 -59
  92. package/dist/developer/upgrades/2.2-to-2.3.md +0 -44
  93. package/dist/developer/upgrades/2.3-to-2.4.md +0 -42
  94. package/dist/developer/upgrades/3.0-to-3.1.md +0 -47
  95. package/dist/developer/upgrades/3.1-to-3.2.md +0 -34
  96. package/dist/developer/upgrades/3.2-to-3.3.md +0 -70
  97. package/dist/developer/upgrades/3.3-to-3.4.md +0 -36
  98. package/dist/developer/upgrades/3.4-to-3.5.md +0 -44
  99. package/dist/developer/upgrades/3.5-to-3.6.md +0 -40
  100. package/dist/developer/upgrades/3.6-to-3.7.md +0 -62
  101. package/dist/developer/upgrades/3.7-to-4.0.md +0 -152
  102. package/dist/developer/upgrades/4.0-to-4.1.md +0 -92
  103. package/dist/developer/upgrades/4.1-to-4.2.md +0 -109
  104. package/dist/developer/upgrades/4.10-to-5.0.md +0 -131
  105. package/dist/developer/upgrades/4.2-to-4.3.md +0 -100
  106. package/dist/developer/upgrades/4.3-to-4.4.md +0 -125
  107. package/dist/developer/upgrades/4.4-to-4.5.md +0 -94
  108. package/dist/developer/upgrades/4.5-to-4.6.md +0 -119
  109. package/dist/developer/upgrades/4.6-to-4.7.md +0 -39
  110. package/dist/developer/upgrades/4.8-to-4.9.md +0 -24
  111. package/dist/developer/upgrades/4.9-to-4.10.md +0 -24
  112. package/dist/developer/upgrades/4.x-to-4.8.md +0 -52
  113. package/dist/developer/upgrades/5.0-to-5.1.md +0 -28
  114. package/dist/developer/upgrades/5.1-to-5.2.md +0 -131
  115. package/dist/developer/upgrades/5.2-to-5.3.md +0 -338
  116. package/dist/developer/upgrades/5.3-to-5.4.md +0 -277
  117. package/dist/developer/upgrades/5.4-to-5.5.md +0 -301
  118. 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