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 then price * 0.9 elsif quantity >= 5 then 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.







