Skip to content
Muhammet Şafak
tr
Journal 3 min read

Shipping a Feature Across Three Layers: API, Web, and Mobile

The end-to-end workflow for delivering a single feature simultaneously across API, web, and mobile layers — lessons from building Looplio.


At Looplio, saying I’m “done” with a feature used to mean finishing a single codebase. Not anymore. The product has three layers: an API written in Laravel, a web application that consumes the same API, and a mobile app written in React Native. A feature isn’t considered shipped until it’s live in all three.

This post describes the workflow I’ve settled into for delivering a single feature across all three layers at once.

I start with the contract

I no longer start with code — I start with the contract. Before writing a single line, I write down which endpoints the new feature will expose, which fields they will return, and which error states they will produce. It takes ten minutes, but it shapes the days that follow: I don’t let the web or mobile side write a single line until the contract is settled.

Before I built this discipline, I kept running into the same class of problem: the API would return a field named date while the mobile side expected dated, and I’d only catch the mismatch during integration. Writing the contract first eliminates most of those errors before they can happen.

Writing the contract has another side effect: it forces me to actually think about what the feature is supposed to do. Sometimes that ten-minute session ends with “actually, I should build this differently.” A revision made before any code is written is far cheaper than one made after.

The API layer — the single source of truth

Business logic lives only in the API. The web and mobile layers carry no rules; they only display data and send requests. Keeping that boundary strict matters, because once a rule is duplicated in two places it will inevitably diverge.

After writing the feature on the API side and getting the tests green, I verify that the responses match the contract exactly:

return UserPlanResource::collection($planlar);

The resource class transforms the database model into the shape I promised in the contract, without leaking raw model internals to the client.

Tests play two roles here: proving that the feature works correctly, and documenting the contract. A test that asserts on the response structure is more trustworthy than a doc page that says “this API returns these fields” — because the test runs, while documentation can go stale.

Web and mobile — showing the same data in two dialects

Once the API is ready, web and mobile proceed in parallel. Both consume the same JSON, but they live in different worlds: React components on the web side, React Native screens on the mobile side. What they share is that I define the API response type identically in both — same field names, same expectations.

The lesson I’ve learned here is not to try writing both clients at the same time. I finish one completely first — usually the web — and confirm there that the contract actually works end-to-end. Then I build the mobile side on top of a contract that has already been proven in the wild.

Writing both clients in parallel seems logical but in practice it produces a working environment where both are half-finished and constantly tangling each other’s problems. Sequential delivery gives each layer clean, focused attention.

My definition of “done”

A feature is done for me when three conditions are met: API tests pass, the feature is exercised through a real user flow on the web, and it’s tested on a real device on mobile. If all three aren’t green, the feature isn’t finished.

The “real device” part matters. Something that works in a simulator or emulator can behave differently on actual hardware — especially around touch targets, keyboard behavior, and network latency. I learned that the hard way a few times, so now it’s a rule.

Shipping across three layers feels slow at first. But moving the contract to the front, keeping rules in one place, and finishing clients sequentially — these three habits reduced the “why does it behave differently here?” bugs to nearly zero. What looks slow turns out to be the fastest path.

Share:

Comments

Sign in with your GitHub account to join the discussion. Comments are stored in GitHub Discussions.

Related Posts

Search the site

Start typing to search posts, projects and pages.

Esc to close Powered by Pagefind