Backend· 5 min read

Designing APIs for Mobile Clients: Pagination, Versioning, Offline

API design for mobile apps: compatibility with old app versions, versioning, cursor pagination, payload size, errors, idempotency and offline sync.

By

Full-Stack, Mobile & AI Engineer

An API built for a website can change any day, because every user loads the latest frontend on their next visit. An API built for a mobile app can't. Some users won't update for months, so every version of your app you've ever shipped is still calling your API, expecting the responses it was built for.

That one fact shapes good API design for mobile apps. This guide covers backwards compatibility and versioning, pagination that survives changing data, payload size, error formats the app can act on, idempotency for unreliable networks, and support for offline sync, whether you use REST, GraphQL or tRPC.

Key takeaways

  • Assume old app versions live forever. Add fields; don't rename or remove them without a plan.
  • Version deliberately (in the URL or a header), and track which app versions use which API version.
  • Use cursor-based pagination so new items don't shift pages while users scroll.
  • Return what screens need, compressed, with stable shapes; avoid both chatty and bloated endpoints.
  • Make writes idempotent with client-generated keys, because mobile networks retry.

Backwards compatibility comes first

Safe changes for mobile clients:

  • Adding new optional fields to responses.
  • Adding new endpoints.
  • Accepting new optional request fields.

Breaking changes that need a plan:

  • Renaming or removing fields.
  • Changing a field's type or meaning.
  • Changing error formats or status codes the app relies on.
  • Making a previously optional request field required.

A habit that helps: generate API types (from OpenAPI or a shared TypeScript package) and run a compatibility check in CI that fails when a change would break an older client.

Versioning

When you must make a breaking change, version it:

ApproachExampleNotes
URL version/v2/bookingsClear and cache-friendly
Header versionAccept: application/vnd.app.v2+jsonCleaner URLs, less visible
App version headerX-App-Version: 3.2.0Lets the server adapt per client and track usage

Send the app version with every request regardless of strategy. It lets you see which versions are still active, adjust behaviour for old clients, and decide when it's safe to retire an API version, ideally alongside a minimum-supported-version check that prompts very old apps to update.

Pagination that survives scrolling

Offset pagination (?page=3) breaks when items are added while a user scrolls: they see duplicates or miss items. Cursor pagination returns a pointer to the last item:

{
  "items": [{ "id": "b_812", "startsAt": "2026-10-14T07:00:00Z" }],
  "nextCursor": "eyJzdGFydHNBdCI6IjIwMjYtMTAtMTRUMDc6MDA6MDBaIiwiaWQiOiJiXzgxMiJ9"
}

The app sends ?cursor=… for the next page. Base cursors on a stable sort (for example timestamp plus ID), and keep page sizes modest (20 to 50 items) for mobile.

Payload size and shape

  • Return what the screen needs. A list endpoint shouldn't include every nested detail of each item.
  • Avoid chatty sequences. If a screen needs four requests in a row, consider one endpoint that returns them together.
  • Compress responses (gzip or brotli) and use sensible image URLs rather than embedded data.
  • Use consistent formats: ISO 8601 timestamps in UTC, amounts as integers in minor units with a currency code, stable enums.

Errors the app can act on

Mobile apps need to decide what to do with an error: retry, ask the user, sign them out, or show a specific message. Return structured errors:

{ "error": { "code": "CLASS_FULL", "message": "This class is full.", "retryable": false } }

Use HTTP status codes correctly (401 for expired sessions, 409 for conflicts, 422 for validation, 429 for rate limits, 5xx for server faults), and keep code values stable so the app can rely on them across versions.

Idempotency for unreliable networks

Mobile requests time out and get retried, by the user or by the app. For any request that creates something or moves money, accept an idempotency key (a unique ID generated on the device) and return the original result for a repeated key. That turns "did my booking go through?" from a support ticket into a non-issue.

From the field: I've architected 15+ full-stack platforms on Node.js / NestJS and Python (Django REST, FastAPI) serving 50K+ daily API requests at sub-200ms p95, and built offline-first sync for an app with 100K+ active users. The APIs that age well are the ones designed for the oldest app version still in someone's pocket, not just the newest one.

Security basics for mobile APIs

Mobile apps can be decompiled and their traffic inspected, so the API must assume every request could be written by hand. Authenticate every request, authorise access per resource on the server, rate-limit sensitive endpoints such as sign-in and password reset, and validate all input. Never rely on secrets embedded in the app to protect an endpoint, and never trust prices, totals or roles sent by the client. If you want extra assurance that requests come from your genuine app, platform attestation services (App Attest on iOS, Play Integrity on Android) add a useful signal, but they complement server-side checks rather than replace them.

Supporting offline sync

If your app works offline, the API needs sync-friendly endpoints:

  • Changes since a cursor ("give me everything that changed after X"), including deletions as tombstones.
  • Batch writes that accept several queued changes in one request, each with its own idempotency key.
  • Server versions on records, so the app can detect conflicts.

REST, GraphQL or tRPC?

OptionStrengths for mobileWatch out for
RESTSimple, cacheable, works everywhereOver-fetching without care
GraphQLClients request exactly what they needCaching and query cost control
tRPCEnd-to-end TypeScript types, great DXBest when client and server share a TypeScript codebase

Any of them works well when the principles above are followed. Choose based on your team and how many different clients consume the API.

FAQ

How should I version an API for a mobile app?

Avoid breaking changes where possible by adding rather than changing fields. When you must break something, introduce a new version (in the URL or a header), keep the old one running while old app versions are in use, send the app version with every request, and pair it with a minimum-version check.

Should mobile APIs use cursor or offset pagination?

Cursor pagination. It stays correct when new items are added while users scroll, which is common in feeds and lists. Offset pagination can show duplicates or skip items as data changes.

Is GraphQL better than REST for mobile apps?

Not inherently. GraphQL lets clients fetch exactly what each screen needs, which helps complex apps with many clients. REST is simpler to cache and operate. Both work well with good pagination, stable error formats and backwards-compatible changes.

Why do mobile APIs need idempotency keys?

Because mobile networks drop and requests get retried. An idempotency key lets the server recognise a repeated request and return the original result instead of creating a duplicate booking, order or payment.

Next steps

If your API is breaking older app versions or making offline features hard, I can review the design and plan changes that keep every app version working. Get in touch, or see how I work as a full-stack developer.

Sources

Bhuwanesh Sisodia

Bhuwanesh Sisodia

Full-Stack, Mobile & AI Engineer

Bhuwanesh Sisodia is a Full-Stack, Mobile & AI Engineer based in Mumbai, with 7+ years of experience and 50+ apps shipped to the App Store and Google Play. He works independently with startups and product teams worldwide.

About meGitHubLinkedInX

Related