A web frontend and its API are usually deployed together. If the API changes, you redeploy both and everyone gets the new version instantly. A mobile app does not work that way. Once a version is installed on a user's phone, it may keep calling your API for months, on unreliable networks, with no way for you to force an update.
That single difference changes how you should design an API. This article covers the conventions we follow when building backends for Flutter apps, including on RaptureTech, a dairy platform where the app is used in operational, real-world conditions.
1. Assume old app versions will keep calling you
Treat every published API response as a contract. Safe changes:
- Adding a new endpoint
- Adding a new optional field to a response
- Accepting a new optional request parameter
Breaking changes that will crash or confuse old app versions:
- Renaming or removing a field
- Changing a field's type (a number becoming a string, a single object becoming a list)
- Making a previously optional field required
- Changing what an error looks like
When a breaking change is unavoidable, version it: /api/v1/ keeps working while /api/v2/ introduces the change. Also add a lightweight "minimum supported version" check the app calls at startup, so you can prompt users to update when you finally retire v1.
2. Use one consistent error format
Mobile developers write error handling once and expect it to work everywhere. Inconsistent errors (sometimes a string, sometimes a dict, sometimes an HTML error page from the server) lead to generic "Something went wrong" messages. Pick one shape and use it for every error:
{
"error": {
"code": "payment_period_closed",
"message": "Payments for September are already closed.",
"fields": {
"period": ["This period is locked."]
}
}
}
The code is stable and machine-readable, so the app can react to it. The message is human-readable and safe to show. fields maps validation errors to form inputs. In Django REST Framework, a custom exception handler gets you this format across the whole API in one place.
3. Paginate everything that can grow
A list that returns 20 items in testing can return 20,000 in production a year later, and a phone on a weak connection will time out long before it finishes. Paginate every list endpoint from day one.
| Style | How it works | Good for |
|---|---|---|
| Page number | ?page=3&page_size=20 | Admin-style lists with page numbers |
| Limit / offset | ?limit=20&offset=40 | Simple lists that rarely change |
| Cursor | ?cursor=eyJpZCI6MTIzfQ | Infinite scroll and feeds where new items arrive often |
For infinite-scrolling lists in a mobile app, cursor pagination avoids duplicate or skipped items when new records are added while the user is scrolling.
4. Design for bad connectivity
Field users, rural areas, basements and moving vehicles all mean slow or dropping connections. A few backend decisions make the app far more forgiving:
- Make writes idempotent. Let the app send a unique request ID (an idempotency key) with actions like "record collection" or "submit order". If the request is retried after a timeout, the server recognises it and does not create a duplicate.
- Return the saved object after a create or update, so the app can update its local state without a second request.
- Support incremental sync with an
updated_sinceparameter, so the app fetches only what changed instead of reloading everything. - Keep payloads lean. Do not embed large nested objects the screen does not need; offer a detail endpoint instead.
RaptureTech's app supports operational dairy-platform workflows, which is exactly where these patterns pay off. The app depends on Django APIs, and when people are recording real operational data, reliable backend logic and forgiving network handling matter more than visual polish.
Send 2026-10-21T04:30:00Z and let the app convert to local time for display. Mixing local times and server times is a classic source of "the entry shows up on the wrong day" bugs.
5. Authentication that survives real usage
Mobile users expect to stay logged in for weeks. The usual pattern is a short-lived access token plus a longer-lived refresh token:
- On login, the API returns an access token (valid for minutes to hours) and a refresh token (valid for days or weeks).
- The app sends the access token with each request.
- When the API responds
401, the app silently calls the refresh endpoint and retries the original request once. - If refresh fails, the app returns the user to the login screen.
Store tokens in secure storage on the device (Keychain on iOS, Keystore-backed storage on Android; in Flutter, a package like flutter_secure_storage), not in plain shared preferences.
6. Document the API as you build it
A machine-readable OpenAPI schema (Django REST Framework can generate one with tools like drf-spectacular) gives the app team an always-current reference and an interactive way to try requests. It also makes it far easier to hand the project to a new developer later.
A checklist before the app goes live
- Every list endpoint is paginated.
- Every error, including 404 and 500, returns the same JSON shape, never an HTML page.
- The API is versioned and the app checks a minimum supported version.
- Create and submit actions are idempotent.
- All timestamps are UTC ISO 8601.
- The API is served only over HTTPS.
- The OpenAPI schema is published to the team.
For the infrastructure side, see our Docker setup for a Django backend serving a Flutter app.