A React interface can look finished while the backend is still a collection of guesses.
The screens render. Forms submit. Navigation works. There may even be a fake login and realistic sample data. But none of that answers the questions the backend must answer:
- Which records exist, and how are they related?
- Which requests require authentication?
- Which records may the current user read or change?
- What happens when input is missing or invalid?
- What exact response and error shapes should the frontend handle?
Connecting a React app to a backend is therefore not mainly a fetch() problem. The real work is turning assumptions scattered across components into an explicit contract.
Start with the UI behaviors
Do not begin with a vague request such as "build the backend for this app." First, inventory what the existing interface promises.
For a client portal, that inventory might include:
| UI behavior | Backend requirement | Access rule |
|---|---|---|
| Sign in | Login route and session or token contract | Public route |
| List cases | Query cases for the current client | Authenticated and owner-scoped |
| Open a case | Load one case and its related records | Case participant only |
| Post an update | Validate and create a case update | Assigned staff only |
| Upload a document | Store metadata and enforce upload rules | Case participant only |
| View an invoice | Return invoice details | Invoice owner only |
For every UI action, write down at least these five items:
- HTTP method and path
- Path, query, and body inputs
- Successful response shape
- Authentication requirement
- Ownership, role, or tenant rule
The fifth item is the one most often hidden by a working demo.
Authentication answers who made a request. Authorization answers which records that identity can access. A route can require a valid token and still expose another customer's data.
Make ownership a backend rule
Suppose a form submits this body:
{
"title": "Updated contract",
"user_id": "user-123"
}
The backend must not treat the submitted user_id as proof of ownership. Anyone can change a request body without changing the interface.
For a user-owned resource, the owner should come from the verified authentication context:
owner_id = authenticated_user_id
The same identity must constrain reads, updates, and deletes:
record.owner_id = authenticated_user_id
For a team application, replace the simple owner rule with an explicit tenant and role rule. Do not accept a tenant ID from the browser without checking that the authenticated user belongs to that tenant and has permission to perform the action.
These rules belong in the backend contract. They should not live only in a React component or in an instruction that the next code-generation pass may forget.
Give React one API contract
Once the backend behavior is defined, avoid manually reproducing it in several frontend files.
The handoff should provide:
- The published API base URL
- Authentication routes and token or session behavior
- Request and response fields
- Required and optional inputs
- Route-specific status codes and error bodies
- Allowed browser origins
- Upload and realtime rules when used
- OpenAPI, generated types, or another machine-readable contract
A small integration function can then stay focused:
const API_URL = import.meta.env.VITE_API_BASE_URL;
export async function listCases(token: string) {
const response = await fetch(`${API_URL}/api/cases`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (response.status === 401) {
throw new Error("Sign in required");
}
if (response.status === 403) {
throw new Error("Access denied");
}
if (!response.ok) {
throw await response.json();
}
return response.json();
}
VITE_API_BASE_URL must point to the generated API base, including any project prefix required by the deployment. It should not be guessed from the current browser origin.
Notice that the function treats 401 and 403 differently. An unauthenticated request and an authenticated but forbidden request are not the same failure. The UI will usually need different behavior for each.
Generated TypeScript clients can reduce manual duplication further, but types are not proof of authorization. A perfectly typed response can still contain another customer's data.
Test the boundary without the frontend
A happy-path button click proves very little about a protected API.
Test the backend directly. For every protected resource, verify these cases:
- A valid identity can perform the intended action.
- A missing or invalid identity cannot perform it.
- User A cannot read, update, or delete User B's row.
- A body-supplied owner or tenant ID cannot override the authenticated identity.
- Missing and invalid inputs return a controlled error.
- Secret and internal fields do not appear in responses.
- The published route matches the documented request and response contract.
For a tenant-based application, add four more checks:
- A tenant member can access permitted tenant rows.
- A user outside the tenant cannot access them.
- A lower role cannot perform an administrator action.
- Changing a tenant ID in the path, query, or body does not change authorization.
These tests catch problems that TypeScript cannot catch.
Separate draft state from published state
Generated backend changes should be treated as candidates, not as truth.
A safer sequence is:
inspect current project
generate a complete candidate
preview without writing
repair validation blockers
apply as a draft
validate the project
publish selected changes
run post-publish tests
Parser success only proves that the candidate can be understood. It does not prove that referenced data models exist, ownership rules are complete, credentials are configured, or the live API behaves correctly.
Keeping apply and publish as separate actions creates a review boundary. It also makes the frontend handoff clearer because everyone can distinguish proposed behavior from the behavior currently exposed by the API.
What generated documentation does not prove
OpenAPI, generated SDKs, and Markdown documentation are useful evidence. They do not create a blanket production guarantee.
They do not prove:
- A customer database migration is safe
- A webhook signature is authentic
- Every third-party credential is configured
- The business rules match the product owner's intent
- Every tenant path is isolated
- The API will meet a particular traffic target
- The system satisfies a compliance standard
Project-specific acceptance tests must still exercise the live endpoints. High-risk authentication, ownership, payment, healthcare, and tenant logic deserves human review and, where appropriate, independent security testing.
A practical definition of done
Before calling a React frontend connected, require all of the following:
- Every important user action maps to a documented endpoint.
- Request and response shapes come from one backend contract.
- Authentication, ownership, roles, and tenant boundaries are explicit.
- Generated changes can be reviewed before publication.
- Draft and published state are distinguishable.
- The frontend consumes a generated or contract-checked client.
- Cross-user and invalid-input tests pass against the published API.
- Browser behavior is verified from the real production origin.
That is the point where the project stops being a clickable mock and becomes something another developer can continue without reverse-engineering the current interface.
The worked contract and public evidence behind this article are available in the original backend handoff guide and the frontend handoff documentation.
Disclosure: This article was prepared with AI assistance. Its technical claims were checked against the linked public contract and the relevant implementation and automated tests.
Top comments (0)