Node.js APIs Are Part of the Interface
API shape determines frontend states, loading behavior, and error copy before a line of UI code is written. The interface starts at the endpoint.
The moment you decide how an API returns an error, you have made a UX decision. You have decided whether the user sees "Email address is already in use" next to the input field, or a generic toast that says "Something went wrong." You made that choice in a route handler, not in a React component, not in Figma. If you designed the API without thinking about the UI, you designed half the interface and handed the other half to someone who has to reverse-engineer your intent.
This is not a theoretical concern. I have shipped both the API and the UI consuming it — on Waco3's proposal platform and on the Tranzport operator interface — and the pattern is consistent: every structural decision made at the endpoint level propagates directly into component complexity, loading states, and error copy. The interface starts at the endpoint.
The Split Model Is the Problem
Most teams run a three-way split: backend engineers design the API contract, frontend engineers consume whatever shape comes back, and UX designers prototype in Figma without knowing what the API actually returns. Nobody owns the state surface — the full set of states the UI must handle for a given user action.
The result is that the frontend engineer ends up as the translator. They get a response shape, compare it to a Figma mockup, and write the glue code that bridges the gap. That glue is not neutral. It carries assumptions about what "loading" means, what "empty" means, and what "error" means. When those assumptions are wrong, users see broken states.
The fix is not better documentation. It is treating API design as interface design from the start.
Four API Decisions That Are Actually UI Decisions
1. Flat vs. nested response shapes
A flat response shape puts the burden of data joining on the component:
{
"shipmentId": "sh_482",
"driverId": "drv_91",
"routeId": "rt_17"
}
The component now has to fire three additional requests, or the backend has to expose a separate aggregated endpoint. Each path adds a loading state the designer never drew.
A nested shape pre-joins the data but adds payload weight and cache complexity:
{
"shipmentId": "sh_482",
"driver": { "id": "drv_91", "name": "Marcus T.", "phone": "+1-254-555-0182" },
"route": { "id": "rt_17", "origin": "Waco, TX", "destination": "Austin, TX" }
}
Neither is universally better. The question is: what does the UI actually render? If every view that shows a shipment also shows driver name and route origin, the nested shape is correct. If there are views that only need the shipment ID, you are over-fetching. This is a UI question being answered in a route handler.
2. Cursor vs. offset pagination
Offset pagination (?page=2&limit=20) looks simple until records are being inserted while a user is paging. A new record at the top shifts everything — page 2 now shows one record that was on page 1, and one record is silently skipped. The UI shows "no more results" before the list is exhausted.
Cursor pagination (?after=cursor_xyz) solves this, but it requires different UI states. You cannot jump to page 7. The "load more" pattern replaces numbered pages. Your empty state logic changes. These are distinct component states that need distinct designs.
Choosing the pagination strategy is choosing which UI problem you are willing to have.
3. Error shape design
This is the one that causes the most quiet damage, because a bad error shape degrades gracefully — the UI just shows less specific information than it could, and nobody files a bug report.
// Unstructured — gives the UI nothing to work with
{ "error": "Something went wrong" }
// Structured — gives the UI a full surface to design against
{
"code": "EMAIL_ALREADY_IN_USE",
"message": "That email address is already registered.",
"field": "email",
"retryable": false
}
The structured shape enables: inline field-level errors (the field key routes the message to the right input), specific copy that matches brand voice, conditional retry buttons (retryable: true on network errors, false on validation errors), and client-side logic keyed on code rather than string-matching message.
The unstructured shape forces the frontend to guess. That guess breaks the first time the backend changes the error string without updating the frontend.
Treat your error shape the same way you treat your design tokens. It is a contract. It flows through every UI state in the application.
4. Streaming vs. batch
A batch endpoint returns the full response when processing is complete. A streaming endpoint begins sending data as it is generated. These produce categorically different UI experiences.
Batch means all-or-nothing. The loading state runs until the response is complete, then everything renders at once. For an AI completion, that means the user waits eight seconds and then sees a wall of text.
Streaming means the UI can begin rendering immediately, give the user something to read while processing continues, and show meaningful progress. Building Waco3's proposal generation on streaming was not a performance decision — it was a UX decision. The difference between a six-second wait and "watching the proposal write itself" is the same underlying latency, handled differently at the endpoint.
You cannot design the streaming UI component and then decide later whether the endpoint streams. The component architecture follows from the API contract.
The Practical Test
Before you finalize any endpoint, run this question against every response it can return:
What does the UI show for this response?
Not "what does the frontend do" — what does the user see? Walk through the 200, the 400, the 401, the 429, the 500, the timeout, the empty array, the partial result. If you cannot answer what the UI shows for each of those, the API is not designed yet.
On the Tranzport dispatcher interface, we ran this exercise on the shipment status endpoint before writing a line of frontend code. We found four states we had not accounted for: a shipment assigned but not yet driver-accepted, a shipment in transit with a stale location, a completed shipment with a disputed signature, and a cancelled shipment with a pending financial adjustment. Each needed a distinct visual treatment. We designed them before the component existed, which meant the component was correct the first time.
The Interface Starts at the Endpoint
Frontend engineers should not just consume APIs — they should be in the room when the API is being designed. Not to own the backend, but because they understand the state surface.
The best full-stack work I have shipped came from treating the API contract as a shared design artifact — something you argue about, revise, and sign off on before either side writes implementation code. The endpoint is not infrastructure. It is interface. Design it like one.