Designing the Backend API for a Mobile App
How to design an API that mobile apps can live with for years: old app versions, token refresh, retries on bad networks, offline sync, cursor pagination and push notification tokens.

A website and its API are deployed together, so the two always match. An app is different. The build you released last spring is still on customers' phones, still calling your server, and it will keep doing so on trains, in elevators and on hotel Wi-Fi. A mobile app backend has to be designed around those two facts: clients you cannot update, and networks you cannot trust.
The decisions below are the ones that are cheap to make before the first release and expensive to retrofit after it.
Old app versions never fully go away
People turn off automatic updates, run old phones and ignore prompts. Whatever you ship in version 1.0 will be sending requests long after 3.0 is out, so three things have to be in place from the start.
The app identifies itself. Send the app version and platform in a header on every request. Without it you cannot tell which builds are still active or decide when an old code path can be retired.
The API is versioned and changes are additive. Put a version in the path (/api/v1/). Within a version, add fields and endpoints freely, but never rename a field, remove it, change its type or change what a value means. On the app side, ignore unknown fields and give unknown enum values a safe default, or the first new order status you introduce will crash every old build.
A forced update is possible. Old builds can only show an "update required" screen if that screen shipped in them. Have the app call a small configuration endpoint at startup:
GET /api/v1/config
{
"min_supported_version": "2.4.0",
"latest_version": "3.1.0",
"maintenance": false
}
Below the minimum, the app blocks and links to the store. Use it rarely, for security fixes and for retiring an API version once the header data shows few people still depend on it.
Authentication and token refresh
The usual pattern is a short-lived access token sent with each request and a long-lived refresh token used only to obtain a new access token. Keep both in the platform's secure storage (the Keychain on iOS, Keystore-backed storage on Android), not in plain preferences or a local database.
Details that cause real incidents:
- Rotate refresh tokens. Issue a new one on every refresh and invalidate the old one, so a stolen token has a short useful life.
- Refresh once, not five times. When a screen fires five requests and the access token has expired, all five receive a 401. The app must run a single refresh and queue the others behind it, or rotation will log the user out.
- Tolerate a lost response. If the refresh succeeds on the server but the reply never reaches the phone, the app retries with the old token. Allow a brief grace period for the previous token so a tunnel does not end the session.
- Issue tokens per device. People want to sign out of a lost phone without signing out everywhere. Laravel Sanctum, for example, issues a named token per device.
- Treat the app binary as public. Any API key or secret compiled into the app can be extracted. Third-party secrets belong on the server.
For sign-in through an identity provider, RFC 8252, OAuth 2.0 for Native Apps sets the practice: run the authorization flow in the system browser, not in an embedded web view, and use PKCE. The wider checklist is in API security best practices.
Timeouts, retries and idempotency on bad networks
On mobile, a request can succeed on the server while the response is lost. The app sees a timeout, the user taps again, and now there are two orders. Solve this on the server, because the client cannot know what happened.
The standard technique is an idempotency key. The app generates a unique ID for each create or pay action and sends it in a header such as Idempotency-Key. The server stores the key with the result, and when the same key arrives again it returns the stored result without repeating the action. Retries then become safe, and the app can retry automatically with increasing delays.
Two habits also help on weak connections. Shape endpoints around screens, so the home screen needs one or two requests and not ten. And return errors the app can act on: the correct HTTP status, a stable machine-readable code, a message a person can read, and field-level details for validation failures.
HTTP/1.1 422 Unprocessable Content
{
"code": "validation_failed",
"message": "Check the highlighted fields.",
"errors": { "email": ["Enter a valid email address."] }
}
Use 401 for an expired session, 403 for a permission problem and 429 with a Retry-After header when rate limiting, so the app can react differently to each.
Offline use and sync
Decide per feature how much offline behavior you need, because the levels differ greatly in effort.
- Cached reads. The app shows the last data it fetched. Support this with
ETagheaders so an unchanged resource returns a 304 with no body. - Queued writes. A small number of actions (submit a form, mark a task done) are stored on the device and sent when the connection returns. Idempotency keys make the replay safe.
- Full two-way sync. Users create and edit records offline on several devices. This needs deliberate design on both sides.
For the third level the API needs four things. Records get IDs generated on the client (UUIDs), so offline items can reference each other before the server has seen them. A changes endpoint returns everything modified since a cursor the server issued, not since a time taken from the phone's clock. Deletions are sent as tombstones, since a row that simply disappears from a list tells the app nothing. And there is a written conflict rule: last write wins per field, or a version number on each record with a 409 response when the client's copy is stale.
Most apps need level one everywhere and level two for one or two actions. Check that before paying for level three.
Pagination that survives new data
Page numbers (?page=3) break in feeds. If new items arrive while the user scrolls, everything shifts and page three repeats entries from page two. Cursor pagination avoids this: the server returns an opaque cursor that points at the last item sent, and the app passes it back.
GET /api/v1/orders?limit=20&after=eyJpZCI6MTA0Mn0
{
"data": [ ... ],
"next_cursor": "eyJpZCI6MTAyMn0",
"has_more": true
}
Sort on a stable key with a tiebreaker, such as creation time and then ID, and cap limit on the server whatever the client asks for.
Push notifications are a backend feature
Push goes through Apple's APNs and Google's Firebase Cloud Messaging (FCM), but your server decides who gets what and keeps the address book. That means a devices table with the push token, platform, user, app version and a last-seen timestamp.
- Register the token on every app start. Tokens change after reinstalls, restores and on the provider's own schedule.
- Delete tokens the provider reports as unregistered. Firebase's guidance on managing registrations also recommends storing a timestamp and pruning entries that have gone stale.
- Detach the token from the user on logout, or the next person on that phone receives the previous user's notifications.
- Expect many users to have no token. Current versions of both platforms ask the user for permission before an app can show notifications.
- Send from a queue with retries, never inside the web request that triggered the event.
Delivery is best effort, so a push must not be the only carrier of information. Treat it as a hint that something changed, and let the app fetch the real data from the API.
Extend your platform or build a new mobile app backend
If you already run a web platform, the app's API is usually a layer on the same code and database, not a second system. The work is in the gaps this article describes: tokens in place of cookie sessions, versioning, device registration and endpoints shaped for screens. A hosted backend service can carry a prototype, and a custom API earns its cost once you have business rules, integrations or data you need to control.
A developer who has built one production API can apply everything here. Bring in a team for payments, multi-device sync or an API over a legacy system, where mistakes show up as lost data. That is the core of our mobile app API integration work, often as a Laravel backend for a Flutter app, and API development and integration covers the server side when other systems are involved.
Frequently asked questions
Should a mobile app use REST or GraphQL?
Either works. REST is simpler to cache, monitor and version, and suits most apps. GraphQL helps when screens need many related objects in one request and several client teams move at different speeds. Old app versions matter in both: a GraphQL schema also has to stay backward compatible.
Do iOS and Android need separate backends?
No. Both apps, and a web front end if you have one, should use the same API. Platform differences are limited to a few areas such as push tokens and store purchase receipts, which the API handles with a platform field.
Should the app call third-party services directly?
Route calls through your backend where a secret key is involved or where you may want to change provider. The app then depends only on your API, and a provider change does not need a store release. Client SDKs designed for direct use, such as maps or a payment form, are the exception.
How long must an old API version be supported?
Until the share of active users on app builds that call it is small enough for you to accept forcing them to update. That is a product decision, and the version header gives you the number to base it on. Announce the cutoff inside the app before you enforce it.
Add worldwincoder.com as a preferred source on Google, or open this article in your AI assistant to use it as a source.
