Four REST API decisions, naming, errors, pagination and versioning, decide how fast your frontend and mobile developers can ship. Here are the rules we follow and why.
Most API pain is not a backend problem. It shows up in the frontend as a loading spinner that never stops, an error toast that says "Something went wrong", or a list that duplicates rows when a user scrolls. The cause is usually a decision made in the first week of the API: a vague name, an error shape that changes by endpoint, a pagination style that cannot survive new data.
This post is for product teams building a REST API that a web app and a mobile app will both use. It covers four areas where small rules save the most time: naming, errors, pagination and versioning. Write them into a one-page style guide that new developers read on day one.
A frontend developer should be able to guess an endpoint before reading the docs. That only works if naming is boring and consistent.
GET /orders returns a list. GET /orders/42 returns one order.POST /orders creates an order. Avoid /createOrder or /getOrderList.GET /orders/42/items is fine. GET /customers/7/orders/42/items/3/options is a sign the item should be addressable on its own.snake_case, every field is snake_case. Mixing createdAt and updated_at in one response costs every client a mapping layer.is_active, has_unread_messages. Avoid status: 1.customer and the app says user, the API should pick one and hold to it.Three field types cause more bugs than any others.
2026-03-14T09:30:00Z. Never send a locale-formatted string. Let the client format it.19.990000000000002 somewhere.When a request fails, the client needs three things: whether to retry, what to show the user, and which field to highlight. A single consistent error body gives all three.
Here is a shape we like:
{
"error": {
"code": "validation_failed",
"message": "Some fields need attention.",
"request_id": "req_8f3a1c",
"details": [
{ "field": "email", "code": "invalid_format", "message": "Enter a valid email address." }
]
}
}
The rules behind it:
400 for malformed input, 401 for not signed in, 403 for not allowed, 404 for not found, 409 for a conflict, 422 for valid syntax but failed business rules, 429 for rate limits, 5xx only when the server is at fault. A 200 with { "success": false } hides failures from every monitoring tool and every HTTP client library.code is for the program, the message is for a human. Frontend developers write if (error.code === "email_taken") and show their own localized copy. If they have to match on message text, you can never reword a message.A mistake we see often: a different error shape per framework layer, so the auth middleware returns { "msg": "..." } and the controller returns { "error": "..." }. Fix it with one error-handling layer at the edge that converts everything into the shared shape.
Any list that can grow needs pagination from day one. Adding it later is a breaking change.
There are two common styles.
Offset pagination uses ?page=3&limit=20. It is easy to build and easy to jump to page 50. The weakness: if a new row is inserted while the user is on page 2, page 3 repeats an item or skips one. Large offsets are also slow in many databases, because the engine still counts past all the skipped rows.
Cursor pagination uses ?limit=20&after=eyJpZCI6MTIzfQ. The server returns an opaque token pointing to the last item seen. Inserts and deletes do not shift results. It is the better fit for feeds, activity logs, chat history and any infinite scroll on mobile.
Our decision rule:
Whichever you choose, the response should say what to do next:
{
"data": [ ... ],
"page": {
"next_cursor": "eyJpZCI6MTQzfQ",
"has_more": true
}
}
The client should never have to compute the next URL or guess whether it reached the end. Set a sensible default limit (for example 20) and a maximum (for example 100), and document both. Also decide the default sort order. An unsorted list is a bug waiting for a database upgrade.
Keep query parameters flat and named after fields: ?status=paid&sort=-created_at. Return 400 for a filter you do not support, rather than silently ignoring it.
You will change the API. The question is whether you break old clients when you do. This matters most for mobile apps, because users run old app versions for months and you cannot force an update instantly.
A practical approach:
/v1/orders. It is visible in logs, easy to route, and easy to test with a link.Deprecation or Sunset response header, and log which clients still call the old endpoint. Remove it only when the logs show nobody is using it.v1, v2 and v3 at once multiplies testing. Aim for two live versions at most.For mobile, add one more safeguard: a minimum supported app version the server can check, so you can show a "please update" screen when an old build can no longer work.
Neither is wrong. REST with the rules above is simpler to cache, monitor and onboard people onto. GraphQL helps when many different clients need very different shapes of the same data. For a typical product with one web app and one mobile app, start with REST and revisit when the pain is real.
Before it is built. Agree the request and response examples in a pull request or a shared document, then let the frontend work against a mock while the backend is built. Disagreements are cheap to fix while they are still text.
If your team is planning an API for a new product, or your current one is slowing down the frontend, we are happy to review it with you. Email hello@risingstation.com or reach us at risingstation.com/contact-us.
Md Kabir Hossain
Co-Founder & CTO