From Idea to Three Platforms: My End-to-End Feature Delivery Flow
The mature, repeatable process I follow when shipping a feature across API, web, and mobile simultaneously.
Shipping a feature across three platforms at once — Laravel API, React web interface, and React Native mobile app — no longer feels like doing it for the first time. A rhythm has formed. This is my first deliberate attempt to write that rhythm down, both as a personal reference and in case it leaves a useful trail for others in a similar position.
The process matured over time. In the early days, every feature was a bit chaotic: which end do I start from, how much detail do I need to define, when do I switch to writing code — none of those answers were clear. Now they’re almost automatic.
Starting point: the contract
The first step for every new feature is writing the API contract. Before writing any code. In a text editor — sometimes a proper OpenAPI draft, sometimes just a few paragraphs of notes:
- Which resource does this feature operate on?
- Which endpoints are needed?
- What do the request and response bodies look like?
- What are the error cases, and what HTTP status codes will they use?
Skipping this step is tempting — the urge to “just start coding, it’s a small feature” is always there. But when I skip it, both the web and mobile sides end up scrambling to adapt to whatever shape the API turned out to be, and that friction is expensive.
The Laravel side: API first
Once the contract is clear, I move to the API layer in Laravel. The core flow:
Migration and model. If a new data structure is needed, migration first, then the model. Getting the relations right from the start reduces the cost of correcting them later.
Validation with Form Request. I keep validation logic in a dedicated Form Request class, not in the controller. This improves both testability and keeps the controller thin:
class StoreEventRequest extends FormRequest
{
public function rules(): array
{
return [
'title' => ['required', 'string', 'max:255'],
'starts_at' => ['required', 'date', 'after:now'],
'ends_at' => ['required', 'date', 'after:starts_at'],
];
}
}
Action class. Extracting business logic from the controller into an Action class makes it possible to invoke the same logic from multiple entry points (web controller, API controller, artisan command).
API Resource. Rather than returning model data directly, I shape it through a Resource class. This gives me control over field exposure and locks the response structure to the API contract.
class EventResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'starts_at' => $this->starts_at->toIso8601String(),
'ends_at' => $this->ends_at->toIso8601String(),
];
}
}
TypeScript types: the bridge
Once the API contract is finalized, I write the TypeScript type definitions for both web and mobile. These types are identical across both projects:
interface Event {
id: number;
title: string;
starts_at: string;
ends_at: string;
}
interface CreateEventPayload {
title: string;
starts_at: string;
ends_at: string;
}
Sharing these through a common package or a monorepo structure is ideal; when that’s not the case, I keep them separately in each project but always treat the API contract as the source of truth.
Web and mobile: parallel, independent
Once the API is working, I can move the web and mobile sides forward in parallel. Both consume the same API, but the interface decisions are independent.
On the web side, fetching data with useQuery and using useActionState for forms (standard since React 19) has become the default.
On the mobile side, FlatList, navigation transitions, and platform-specific behavioral differences (iOS vs Android) each get their own attention. I expect these differences from the outset — they’re not surprises.
Testing and sign-off
The moment I consider a feature complete: there are unit/feature tests on the API, the core flow works in the web interface, and it has been verified on physical iOS and Android devices. Until all three conditions are met, the feature is not “done.”
The real value of this rhythm isn’t speed. It’s the ability to quickly isolate where a problem originates — is it in the API, the web, or mobile? — and to move independently within each layer. Repeatability is what makes that possible.
Comments
Sign in with your GitHub account to join the discussion. Comments are stored in GitHub Discussions.