Headless and API First Solidus: The Prerequisite for a Mobile App

There is a conversation I have had so many times I could set it to music. A shop has been live for about eighteen months. It is doing fine. And then somebody in the business, usually after a trade show or a conversation with a competitor, says the word app. Or marketplace. Or kiosk for the showroom. Or a second brand that should sell the same stock from a different looking site. And the owner rings me and asks how hard it would be. The honest answer depends almost entirely on a decision that was made, or more often not made, before the shop was built: whether the shop was built as a website that happens to sell things, or as a system that sells things which happens to have a website in front of it. That distinction is what people mean by headless, when they mean anything at all by it. This article is about what it actually means, when it is worth the trouble, when it is not, and how I build shops on Solidus so that the app question has a cheap answer whenever it turns up.

The question that arrives about eighteen months in

There is a conversation I have had so many times I could set it to music. A shop has been live for about eighteen months. It is doing fine. And then somebody in the business, usually after a trade show or a conversation with a competitor, says the word app. Or marketplace. Or kiosk for the showroom. Or a second brand that should sell the same stock from a different looking site.

And the owner rings me and asks how hard it would be.

The honest answer depends almost entirely on a decision that was made, or more often not made, before the shop was built: whether the shop was built as a website that happens to sell things, or as a system that sells things which happens to have a website in front of it. That distinction is what people mean by headless, when they mean anything at all by it.

This article is about what it actually means, when it is worth the trouble, when it is not, and how I build shops on Solidus so that the app question has a cheap answer whenever it turns up. I wrote the general case for the platform in the Solidus comparison piece. This one is narrower and, I think, more useful if you already know you are building on something you own.

What headless actually means, in one sentence

The storefront is a client of the shop. It is not the shop.

That is the whole idea. Everything else is detail, and most of the detail is vendors dressing it up.

In a conventional shop, the thing that renders the product page is the same thing that knows what the product costs, whether it is in stock, what promotions apply, and what happens when you press add to basket. The rules and the pixels live together. That is fine right up until you want a second set of pixels, at which point you discover that the rules are tangled into the first set and you have to either duplicate them or pull them apart.

In a headless shop, the rules live in one place and expose themselves through an interface, almost always a JSON API over HTTP. The website talks to that interface. So does the app. So does the marketplace feed, the kiosk, the partner who wants to sell your stock on their site, and the internal tool your warehouse uses. None of them know anything about pricing or stock or promotions. They ask, and they are told.

If you have ever used a bank's website and its app and noticed that your balance is the same in both, you have used a headless system. The balance is not in the app. The app asks.

Headless the marketing word versus headless the contract

Now the part the vendors do not tell you.

Almost every platform on the market now says it is headless. Shopify has a storefront API. Shopware has a store API. Even WooCommerce has a REST API of sorts. So in the marketing sense, everyone is headless, and the word has stopped meaning anything.

What matters is not whether an API exists. It is whether the API is the contract, meaning the thing every client actually goes through, or a side door bolted on next to the real logic. The difference shows up in three questions.

First, does the platform's own storefront use the API? If the vendor's website goes through a different, privileged path and the public API is a translation of it, then the API will always lag. Features land on the website first and reach the API when someone gets round to it. You will find this out when the promotion you configured works on the web and not in the app.

Second, can you do everything through the API that you can do through the admin? Creating an order with a custom price, applying a promotion that was not designed for self service, splitting a shipment, refunding part of a line. If the answer is "most things", the answer is no.

Third, who owns the API? If it is the vendor's, they can rate limit it, version it, deprecate it, and charge for it, and they will do all four eventually. If it is yours, in your codebase, running on your server, then it changes when you change it.

Solidus, to its credit, passes the first two and is one of the few platforms where the third is unambiguous, because there is no vendor. The API is a Rails engine that ships with the core, and the storefront I build is just another consumer of the same models. I will come back to what that means in practice.

When headless is worth it

Let me be concrete, because "flexibility" is not a business case.

You want a mobile app. This is the big one and the reason the article has the title it has. An app is a second storefront that happens to run on a phone. It needs the catalogue, the basket, the checkout, the account, the order history, and it needs them to behave exactly as the website does, because the same customer will use both. Without an API you are building all of that twice, and the second copy drifts from the first within a quarter. I will say more about the app itself at the end.

You want to sell on marketplaces or through partners. Amazon, Kaufland, Otto, the trade portal your industry uses, a retailer who wants to list your range. Every one of these wants a feed of products and stock and wants to push orders back. That is an API in both directions. Bolting a feed export onto a shop that was not built for it usually means a nightly CSV and a stock count that is wrong for twenty three hours a day.

You want a second brand on the same catalogue. A premium and a value label. A B2B site and a consumer site. A German shop and a Swiss one with different pricing and legal texts. If the rules live in one place, the second storefront is a design job. If they live in the templates, it is a second shop.

You have a physical presence. A showroom, a market stall, a pop up. A kiosk or a tablet at the till that sells from the same stock, at the same prices, with the same promotions, and puts the order into the same system so the warehouse sees one queue. That is an API client. A cheap one, once the API exists.

Your systems need to talk. ERP, warehouse management, accounting, a CRM, an email tool that wants to know about abandoned baskets. Every integration is easier when there is one well documented way in and out of the shop, and nearly impossible to keep tidy when each one scrapes its own path through the database.

If two or more of these are on your roadmap, and I mean actually on it rather than mentioned once in a meeting, the API is not optional. It is the thing that makes the rest affordable.

When headless is not worth it

Here is the other side, because I have watched people spend real money on architecture they never used.

A single storefront and no plans. If you sell through one website, you have no intention of building an app, you do not sell on marketplaces, and your integrations are Stripe and an email provider, then a fully decoupled headless build is a tax. You will maintain two applications instead of one, a frontend and a backend, deploy them separately, debug them separately, and pay two sets of hosting. For nothing.

A small team. Fully headless means a JavaScript frontend talking to a Ruby backend, and that means two skill sets, two build pipelines, and two places where a bug can hide. A team of one or two is better off with one application.

A large catalogue that lives on search traffic. This is the one that surprises people. If your revenue depends on Google finding ten thousand product pages, you need those pages rendered on the server, fast, with correct structured data, and you need it to be boring. A client side rendered frontend can do this, with effort, using server side rendering frameworks, but now you have added a rendering layer to solve a problem the Rails app never had. I have seen shops lose a third of their organic traffic in a "headless migration" and take a year to get it back.

Because the agency said so. Fully decoupled builds are more expensive, take longer, and need more people. Those are three reasons an agency might like them that have nothing to do with your business.

So the answer is not headless or not. The answer is the version of headless that gives you the contract without the cost of running two applications. Which brings me to what I actually build.

The hybrid I actually build

Every shop I build has the same shape, and I have arrived at it by getting it wrong in both directions.

There is one Rails application. It contains the Solidus core, which owns products, variants, prices, stock, promotions, orders, payments, shipments and taxes. It contains a server rendered storefront, meaning ordinary Rails controllers and views that produce HTML, which is what your customers see and what Google indexes. And it contains a JSON API, which is a second set of controllers that expose the same models to anything that is not a browser.

The important part is what those two sets of controllers have in common, which is everything below them.

When a customer on the website adds a hamper to their basket, the storefront controller calls the same service object that the API controller calls when the app does it. The same promotion rules run. The same stock check happens. The same line item comparison decides whether this is a new line or a quantity increase on an existing one. There is no way for the web and the app to disagree, because there is no second implementation to disagree with.

I call this API first even though the storefront is not a client of the API in the HTTP sense. It is API first because the API is designed as a first class citizen from the start, the rules are kept out of the views, and nothing the storefront can do is impossible through the API. If I later decided to rebuild the storefront as a JavaScript application, I could, and it would talk to the same endpoints the app does. I have never needed to.

What you get from this is one application to deploy, one to monitor, one to secure, one to test. Server rendered pages for search and speed. And the moment someone says app, the answer is "the API is already there, we need to build the app", which is a different conversation from "we need to build the API and then the app".

What Solidus gives you on day one

Solidus ships with an API engine as part of the core. It is not an extension you might or might not install. Out of the box you get most of what a client needs.

Catalogue endpoints. Products, variants, taxons, images, prices, stock. Paginated, filterable, and returning enough that a product listing page in an app can be built directly from them.

Cart and checkout. Create an order, add and remove line items, advance the order through its states, set addresses, pick a shipping rate, add a payment, complete. The state machine that drives the web checkout is the same one the API drives. If your web checkout has a custom step, and mine usually do, the API has that step too, because the step lives in the order, not in the view.

Accounts and authentication. Every user gets an API key. Clients send it as a bearer token. Guest orders work with an order token. If you want something more elaborate, a mobile app usually wants short lived tokens with refresh, the pieces are there to build it on.

Admin operations. Orders, shipments, refunds, stock movements, promotion management. Everything your warehouse tool or your accounting integration needs to do is exposed, and it respects the same permissions your admin users have.

An event bus. Solidus publishes events when things happen: an order completes, a shipment ships, a payment fails. You subscribe to those events and do whatever you need, which for integrations usually means posting a webhook to somebody else's system. There is no built in outbound webhook manager, and I have never missed one, because a subscriber that posts JSON is fifteen lines and I can test it.

That is a lot to get for free, and it is the reason I say the API is there from day one rather than something you add.

What you have to design yourself

And here is what you do not get, which is the part a proposal should be honest about.

Versioning. The moment an app is in the store, you cannot change an API response without breaking phones you do not control. Somebody will be running the version from eight months ago. You need a versioning strategy before the first app ships, whether that is a version in the URL, a header, or a policy of only ever adding fields and never removing them. I usually do the last, with a hard version bump reserved for the rare change that cannot be additive.

Rate limits. A public API will be hit by things that are not your app. Scrapers, competitors, a partner's buggy integration that retries every second. You need per client limits, and you need them to fail politely with a proper status code rather than by falling over. Rack Attack, a small config, and a place to see who is hitting what.

Caching. Product data changes rarely and is read constantly. An app that fetches the catalogue every time someone opens it will make your server work harder than your website does. HTTP caching headers, conditional requests, and a cache layer for the read heavy endpoints are not optional once real traffic arrives.

Idempotency for anything that costs money. I give this its own section below because it is the one that hurts.

Image delivery. An app wants images in three or four sizes, in modern formats, from a CDN, fast. Solidus stores images and can produce variants on demand, but the delivery pipeline, the CDN in front of it, and the URLs the API hands out are design decisions you make once and live with.

Search. The catalogue endpoints filter, but they are not a search engine. If your app needs "type three letters and see suggestions", you need a search index and an endpoint in front of it. I add this when the catalogue justifies it and not before.

Documentation. Not for you. For the person who builds the app after the person who built it has left. An API without a written contract is a verbal agreement, and verbal agreements do not survive staff changes. I generate the docs from the tests, which I cover in my piece on testing APIs.

None of these is hard. All of them are things I have seen missed, and each one missed becomes a rebuild later.

The Gift Bow as the running example

Let me make this concrete with the first of my demo shops, a gift and hamper store called The Gift Bow. I built it partly to have a real shop to point at and partly to see how far the "API ready from day one" discipline holds when the features get unusual. They got unusual.

The shop is bilingual, English and German, with the language chosen on the first visit from the browser and then from the visitor's country. It has five explicit price lists, in pounds, euros, US, Australian and New Zealand dollars, with proper charm pricing in each rather than a converted number, and the currency follows the visitor's country too. It has a build your own hamper feature where you pick a container at a fixed price, say sixty pounds for six items or a hundred for eight with at most two bottles, and fill it from the catalogue. The chosen contents ride along on the line item as structured data, and the basket knows that two hampers with different contents are two different things, not a quantity of two. The homepage has promotion cards managed in the admin, with scheduling, so the festive collection can be set up in October and appear on the first of November without anyone touching anything. Some products can be bought on a subscribe and save basis at a chosen interval. Delivery rules vary by region.

Every one of those features was built as a rule on the order or the product, not as a view. That was the discipline, and it paid off in ways I did not expect.

The currency and language selection is not a storefront trick. It is a pricing option on the order, which means an app that says "this customer is in Austria" gets euro prices and German text from the same endpoint the web uses, without a line of app specific logic. The hamper builder is a service that takes a container variant and a list of picks and produces a line item with the contents attached. The web form calls it. An app would call it through the API with a JSON body that looks almost exactly like the form. The line item comparison, the rule that says "these two hampers are different", lives in the model, so a basket created through the API behaves identically to one created on the web. The promotion cards are records with start and end dates; an app home screen can fetch the live ones and render them natively, and they will switch over at the same moment the website does.

I did not build an app for the Gift Bow. I do not need one. The point is that when I check what it would take, the answer is: design the screens, build the app, point it at what is already there. There is no "first we have to extract the business rules from the templates" phase, and that phase is where the money goes on shops that were built the other way.

Payments, idempotency and the double charge nobody wants

This is the section I would make every owner read if I could only pick one.

On a website, a payment happens once because the browser submits a form once and then shows a confirmation page. If the connection drops after the payment but before the confirmation, the customer sees an error, refreshes, and usually finds their order in their account. Annoying, but rare and recoverable.

On a mobile app, connections drop constantly. Tunnels, lifts, the walk from the office to the car park. An app that sends "complete this order with this payment" and does not hear back will, if it is written naively, send it again. Now the customer has been charged twice, or the order exists twice, and you find out from a one star review.

The fix is a well understood pattern called idempotency. The app generates a unique key for each attempt, sends it with the request, and the server remembers which keys it has already processed. A retry with the same key returns the original result instead of doing the work again. Stripe uses exactly this on its own API, which is why you can retry a Stripe call safely.

Solidus does not do this for you on its checkout endpoints. It is a few dozen lines to add: a table of seen keys, a check at the top of the completion action, a stored response. I add it to every shop where an API client might ever complete an order, which is every shop, because the cost of adding it is an hour and the cost of not having it is a customer you will never see again and a refund your bookkeeper has to explain.

While we are here: the same applies to anything that changes stock, sends an email, or creates something the customer will see. Any endpoint that has a side effect needs to be safe to call twice.

Images, search and the things people forget

Two smaller points that always surface late.

Images first. A web page loads a product image once, at the size the layout needs. An app on a fast phone wants a thumbnail in a grid, a medium image on the product screen, and a full size one when you pinch to zoom, and it wants them cached so scrolling is instant. If the API hands out one URL to one large original, the app is slow and the server is busy resizing. If the API hands out a set of URLs to pre generated sizes on a CDN, the app is fast and the server never sees the request. This is a decision about how the API describes an image, and it has to be made before the first client is written, because changing it afterwards means changing every client.

Search second. The API can filter products by taxon, by price range, by name containing a string. That is enough for a category screen. It is not enough for a search box, because a search box needs typo tolerance, ranking, and results in under a hundred milliseconds. For that you need an index, and the index needs to be kept in sync with the catalogue, and the sync needs to be tested. It is a project, a small one, and the mistake is not doing it. The mistake is promising the app a search box before it exists on the API.

The cost of doing it later

I want to put a number on the difference, because "build it right the first time" is advice everyone gives and nobody costs.

Building a Solidus shop so that the rules live in services and models and the API is a first class consumer adds, in my experience, somewhere between five and ten percent to the build. That is the cost of the discipline: a service object instead of logic in a controller, a test that exercises the API endpoint as well as the web form, a decision about image URLs taken early. On a sixty thousand euro build, three to six thousand.

Retrofitting an API onto a shop where the rules have grown into the views is a different kind of project. You are not adding an API. You are finding every place a business rule lives, which is usually more places than anyone remembers, pulling each one out into something both the web and the API can call, and doing it without changing the behaviour of a live shop. It is archaeology with a deadline. I have done it three times and it has never come in under a third of the original build cost, and twice it was closer to half.

So the honest arithmetic is: five to ten percent now, or thirty to fifty percent later, plus the delay, plus the risk of breaking something that works. And that is before the app itself, which is the same cost either way.

The reason people choose "later" is not that they have done this maths and decided against it. It is that nobody put the option in front of them at the start. Consider this me putting it in front of you.

What "API ready from day one" means in practice

Since I keep using the phrase, here is what it actually involves on a build, in the order I do it.

Business rules go in services and models, never in views or controllers. If a controller does more than take input, call something, and render, it is wrong.

Every feature that the web storefront can do gets an API test as well as a web test. Not a full second implementation, just a test that proves the endpoint does the same thing. This is the check that stops drift.

Responses are designed to be added to, never changed. New fields are fine. Renaming or removing a field is a version bump, and version bumps are rare and deliberate.

Anything that costs money or has a side effect accepts an idempotency key.

Images are described as a set of sizes on a CDN, not one original.

There is a rate limiter and a way to see who is hitting what.

The contract is written down, generated from the tests so it cannot go stale.

None of that is exotic. It is the difference between a shop that can grow a second face and a shop that cannot, and it costs almost nothing if you start that way.

The app itself, briefly

I have said very little about the app, deliberately, because the app is the easy part once the foundation is right. Since people ask: I build them in React Native, one codebase for iOS and Android, talking to the same API as everything else. An app for a shop with a solid API is a design and build project measured in weeks. The same app for a shop without one is measured in quarters, and most of those quarters are spent on the shop, not the app.

Whether you should have an app at all is a separate question, and my answer is more cautious than you might expect from someone who builds them. Most shops do not need one. Repeat purchase businesses, subscription businesses, anything where the customer comes back monthly and would value push notifications and a saved basket, those benefit. A shop that sells someone a sofa every seven years does not. I will write that up properly another time.

Where this leaves you

If you are building a new shop, insist on the hybrid. One application, server rendered storefront, first class API, rules in one place. Ask your developer to show you the API tests alongside the web tests, and ask what happens when the app retries a payment. If they look blank, keep asking.

If you already have a shop and the app question has arrived, get someone to look at where the rules live before anyone quotes on the app. The quote for the app is only half the number, and it is the smaller half if the shop was built as a website that happens to sell things.

And if you want to see what API ready from day one looks like with real features on it, the demo shops are live, the hamper builder works, and I am happy to walk through the code with you.

If you are wondering whether your shop could carry an app, a marketplace feed or a second storefront without a rebuild, let's look at it together. I will tell you honestly whether the foundation is there, and what it would take if it is not.