PlanPal had two sets of controllers: web controllers returning Blade views with custom AJAX routes, and API controllers returning JSON for the mobile app. Same business logic, duplicated across both. When I fixed a bug in one, I had to remember to fix the other.

The Migration

I migrated the web frontend from custom AJAX routes to the same REST API the mobile app uses. The key was a feature flag for zero-downtime rollout:

// config/api.php
'unified_api' => env('USE_UNIFIED_API', false),
// Alpine.js component
const baseUrl = window.appConfig.useUnifiedApi ? '/api/v1' : '/planner';

async function fetchAppointments(date) {
    const response = await fetch(`${baseUrl}/planner/${date}/appointments`);
    return await response.json();
}

I migrated one feature at a time -- appointments first, then tasks, then contacts -- testing each before flipping the flag for the next. The old web routes stayed in place until all features were migrated.

The Result

Metric Before After
Controller files 45 web + 45 API 45 API only
Route definitions ~180 total ~90 total
Bug fix locations 2 (web + API) 1

The standardized JSON response format ({success: true, data: {...}}) works for both Alpine.js fetch() calls and React Native API clients. One set of tests covers both platforms.

The Lesson

If your web frontend makes AJAX calls that return JSON, it's already an API consumer -- just an unofficial one. Formalizing the API and having both platforms consume it eliminates an entire class of "fixed on web but not mobile" bugs.