Profile    Mohammed Shiroz Status   Loading  
Logo
Share This
Back to blog
Filter by:
Tags
//Article title

One API, Three Apps: Keeping a Mobile App and a Web Portal in Sync

22 Mar 2026Category : Blog

About Post

When you deploy a web app, every user gets the new version within minutes. When you release a mobile app, users get it whenever they feel like updating. Which, for some of them, is never.

That one difference shapes everything about building an API that serves both. Kate PMS, the property management system I lead at Kate, has three clients talking to the same backend: a web portal for staff, a staff app, and a tenant app built with React Native. Contracts, payments and maintenance requests flow through all of them.

Keeping them in sync isn't one clever trick. It's a handful of rules that sound obvious and are surprisingly easy to break on a busy day. Here they are.

The real problem: you never have just one version in production

The web portal is easy. It ships with the backend, so the front end and the API always match.

The mobile apps are different. A release goes through app store review. Users have automatic updates turned off, or they're on an older phone, or they simply haven't opened the app in weeks. At any moment, several versions of each app are out there calling your API, and each one expects the API to behave the way it did when that version was built.

So the mental shift is this: the API isn't for the current app. It's for every version still installed.

Rule 1: Change the API by adding, not by editing

The safest change is an additive one. A new field, a new endpoint, a new optional parameter: older apps simply ignore what they don't know about.

The dangerous changes are the ones that look like tidying up:

  • Renaming a field from tenant_name to tenant.
  • Changing a value's type, like an amount from a number to a string.
  • Making an optional request field required.
  • Changing what a status value means.

Every one of these works perfectly with the newest app and breaks the older ones, often in ways that only show up as a blank screen for a user you'll never hear from.

In Laravel, API Resources make this discipline easier, because the response shape is defined in one place instead of leaking straight from the model:

public function toArray(Request $request): array
{
    return [
        'id' => $this->id,
        'status' => $this->status->value,          // older apps read this
        'status_label' => $this->status->label(),  // added later, ignored by them
        'due_date' => $this->due_date->toDateString(),
    ];
}

Adding a column to the database doesn't change the API by accident, and renaming one doesn't break it by accident. The resource is the contract.

Rule 2: Version when you truly have to break

Sometimes a change can't be additive: a flow is redesigned, or a response needs a different structure. That's what versioning is for. A prefix like /api/v1 and /api/v2, with separate route groups and resources, lets the new app use the new shape while older apps keep working on the old one.

The cost is that you now maintain two versions. So version rarely, deliberately, and with a plan for retiring the old one. If every small change becomes a new version, you'll end up maintaining a museum.

Rule 3: Know which versions are actually out there

You can't retire an old version safely if you don't know who's still using it. Have every app send its version with each request, in a header such as X-App-Version, and log it.

Now "can we remove the old field?" stops being a guess. You can see whether anyone is still on the version that reads it.

Rule 4: Keep a forced update as a safety valve

Occasionally, you need old versions gone: a serious bug, a payment flow that must change, a legal requirement. For that, the API needs a way to say "this version is no longer supported, please update".

A simplified version of the idea, as Laravel middleware:

public function handle(Request $request, Closure $next): Response
{
    $version = $request->header('X-App-Version');
    $minimum = config('mobile.min_supported_version');

    if ($version && version_compare($version, $minimum, '<')) {
        return response()->json([
            'message' => 'A new version of the app is required.',
            'update_required' => true,
        ], 426); // 426 Upgrade Required
    }

    return $next($request);
}

The app treats that response as a signal to show a friendly full-screen "please update" message with a link to the store, rather than a cryptic error.

The key is that this logic has to exist before you need it. A forced-update check you add in version 3 can't help you with users stuck on version 1. Build it into the very first release.

It's also worth having a softer level: a "recommended" version that shows a dismissible banner, so you can nudge people without blocking them.

The rule I'd tattoo on every mobile API: ship the forced-update check in version 1.0, and hope you rarely use it. It's the one feature you can never add retroactively.

Rule 5: The server owns validation and business rules

With three clients, it's tempting to validate in each one. Then the rules drift: the web portal accepts a date the tenant app rejects, and the staff app allows a value the server later chokes on.

The fix is to make the server the single source of truth:

  • Form Requests define the rules once. Every client gets the same 422 response with the same field errors, and each app displays them.
  • Client-side checks are for user experience only. "This field is required" can be instant in the app, but the server still decides.
  • Send lists from the server. Things like maintenance categories or document types come from an endpoint, not hard-coded in the app. Add a new category on the server and every app version shows it, with no store release.

That last point is underrated. Every piece of data you hard-code into a mobile app becomes something you can only change by shipping a new version.

What about over-the-air updates?

With Expo, JavaScript-only changes can ship as over-the-air updates without going through the store, which closes the gap a lot. But changes to native code still need a store release, and users still have to open the app to receive the update. It's a helpful tool, not a replacement for a backwards-compatible API.

Test the contract, not just the code

Feature tests that assert the response shape catch accidental breaking changes before they ship:

$this->getJson('/api/v1/contracts/'.$contract->id)
    ->assertOk()
    ->assertJsonStructure(['data' => ['id', 'status', 'due_date']]);

When someone "tidies up" a field name, this test fails in CI, during pull-request review, instead of on someone's phone.

The checklist

  • Design the API for every installed version, not just the latest.
  • Add fields; don't rename or remove them in place.
  • Version only for real breaking changes, and plan the retirement.
  • Send the app version with every request and log it.
  • Ship a forced-update mechanism from day one.
  • Keep validation and lists on the server.
  • Test response shapes so breaking changes fail in CI.

If you maintain an API for mobile apps, how do you handle old versions: forced updates, long-lived API versions, or something else entirely?

Comments (0)
Leave your review

Thanks for your valuable comments. Your comments has been updated and appreciate your getting in touch...

01. About Shiroz

Mohammed Shiroz

Hi, I'm Mohammed Shiroz, a software engineer and AI enthusiast from Sri Lanka who turns ideas into intelligent, real-world solutions. With over 9 years of hands-on experience, I currently lead real estate ERP development at Kate Group, a...

03.My Projects

04. Categories

Ready To order Your Project ?

Get in Touch
Close