Building an Ecommerce Store on Spree: From Monolith to Headless
A client asked to add a "buy 3, get the 4th free" promo with order history validation. In Shopify, this would require a third-party app with a separate subscription; in Spree — 50 lines of a decorator. Spree Commerce is an open-source Rails Engine we use for projects requiring deep customization. In this article, we'll show how to set up monolith or headless architecture and cover real examples.
Why Choose Spree: Monolith or Headless?
Spree embeds into a Rails application as an Engine and shares the same database. Starting from version 4.3, headless mode via REST API v2 allows using Spree as a backend for React/Vue frontends. This suits both simple server-rendered stores and complex SPAs. Let's compare.
| Criteria | Monolith (classic) | Headless |
|---|---|---|
| Architecture | Rails Engine with server-rendered storefront (ERB + Turbo) | Spree provides API, frontend deployed separately (Next.js, Nuxt) |
| Team | Rails developers + possibly frontend | Separate frontend team, backend Ruby |
| Performance | SSR, easy caching | Flexibility, edge functions possible |
| Development time | Faster, fewer moving parts | Longer but more scalable |
| When to choose | Small teams, simple stores, no mobile app requirements | Complex projects, multiple client apps, high load |
How to Set Up Spree for Your Project?
Environment setup includes installing required gems and running generators. Standard installation: add gems, run generators, migrate database. Common pitfalls: incorrect database config or gem version conflicts.
# Gemfile
gem 'spree', '~> 4.10'
gem 'spree_auth_devise', '~> 4.6'
gem 'spree_gateway', '~> 3.10'
gem 'spree_backend', '~> 4.10'
gem 'spree_sample', '~> 4.10' # sample data
# For headless: gem 'spree_api', '~> 4.10'
bundle install bin/rails g spree:install bin/rails g spree:auth:install bin/rails db:migrate bin/rails db:seed After installation, you get:
-
/admin— admin panel -
/api/v2/storefront— REST API for frontend -
/— classic storefront (ifspree_frontendis installed)
Spree Data Model
Spree's database revolves around key entities: stores (multi-store support), categories (nested via ancestry), products with variants (SKU, price, options), orders with line items, payments and shipments, users.
Spree::Store
├── Spree::Taxon (categories via ancestry)
├── Spree::Product
│ ├── Spree::Variant
│ ├── Spree::Price
│ └── Spree::Image
├── Spree::Order
│ ├── Spree::LineItem
│ ├── Spree::Payment
│ └── Spree::Shipment
└── Spree::User (via spree_auth_devise) Building a Headless Store with Next.js and Spree API
For headless projects, we use @spree/storefront-api-v2-sdk and work via REST API. In one project, we replaced the monolith storefront with Next.js 14 and ISR, achieving LCP under 0.8 seconds — a significant improvement in Core Web Vitals. React Server Components allow fast server-side rendering of product pages.
// lib/spreeClient.ts
import { makeClient } from "@spree/storefront-api-v2-sdk";
export const client = makeClient({
host: process.env.NEXT_PUBLIC_SPREE_URL!,
});
// Fetch products
const products = await client.products.list(
{
include: "default_variant,images,taxons",
filter: {
taxons: taxonId,
},
},
{
sort: "name",
page: 1,
per_page: 24,
}
);
// Create cart
const cart = await client.cart.create();
const orderToken = cart.success().data.attributes.token;
await client.cart.addItem(
{ orderToken },
{
variant_id: variantId,
quantity: 1,
}
);
Customizing Business Logic Without Forking
Spree uses the decorator pattern: we add methods and associations in a module that is mixed into the model. This extends functionality without touching the core.
# app/models/spree/product_decorator.rb
module Spree
module ProductDecorator
def self.prepended(base)
base.has_many :bundle_parts, class_name: "Spree::BundlePart", foreign_key: :bundle_product_id
end
def bundle?
bundle_parts.any?
end
def effective_price_for(quantity)
if quantity >= 10
price * 0.9
elsif quantity >= 5
price * 0.95
else
price
end
end
end
end
Spree::Product.prepend(Spree::ProductDecorator)For promotions, use the built-in Spree::Promotion system. For example, a "15% off orders over 2000 rubles" rule:
promotion = Spree::Promotion.create!(
name: "Summer 15% discount",
code: "SUMMER15",
starts_at: Date.today,
expires_at: 3.months.from_now,
usage_limit: 1000
)
promotion.actions.create!(
type: "Spree::Promotion::Actions::CreateAdjustment",
calculator: Spree::Calculator::FlatPercentItemTotal.create!(preferred_flat_percent: 15.0)
)
promotion.rules.create!(
type: "Spree::Promotion::Rules::ItemTotal",
preferred_operator: "gte",
preferred_amount: 2000.0
)This flexibility allows complex promos without extra plugins.
What's Included in the Work: Full Development Cycle
We offer turnkey development. Project stages:
| Stage | Description | Duration (days) |
|---|---|---|
| Installation & configuration | Rails app, Spree Engine, database | 2–3 |
| Catalog + product import | Rake tasks, CSV/API import | 4–8 |
| Custom business logic | Decorators, promotions, shipping | 5–10 |
| Frontend (Headless) | Next.js + Spree SDK | 10–20 |
| Payment integrations | 2–3 providers | 4–6 |
| Admin panel customization | Additional sections, reports | 3–5 |
| Total | 28–52 |
The result includes: API documentation, repository and server access, client team training, 30-day warranty on bugs. Our engineers have 10+ commercial Spree projects.
Payment Integration and Multi-Currency
spree_gateway provides ready adapters for Stripe, Braintree, PayPal. For YooKassa or Tinkoff, we write a custom gateway (implementing Spree::Gateway interface). Multi-currency is configured via store attributes: set supported_currencies and supported_locales. Prices are tied to the variant's currency.
Common Mistakes When Starting with Spree
- Not using decorators — editing core Spree directly makes updates impossible.
- Forgetting N+1 queries in API — always include
includein requests. - Ignoring admin panel performance — for large catalogs, Elasticsearch is needed.
- Not setting up caching — Redis is mandatory for production. Without it, the store slows down with 1000+ products.
How to Choose Between Monolith and Headless?
If your team is strong in Rails and you need a typical store, go with monolith — it's faster to start. If you plan a mobile app, complex frontend, or high load, headless offers flexibility but requires more development resources. Order Spree store development — we will contact you within a day. Get a consultation via the form on the website or in messengers.







