Model request states explicitly
A frontend integrating an API needs loading, success, empty and error states. Keep the transport contract separate from the presentation so a component does not quietly assume every HTTP response contains a valid array.
Read helper with cancellation
export async function loadCourses(signal) {
const response = await fetch('/api/courses', {
signal,
headers: { Accept: 'application/json' }
});
if (!response.ok) throw new Error('Courses could not be loaded');
const data = await response.json();
if (!Array.isArray(data.courses)) throw new Error('Unexpected course response');
return data.courses;
}fetch does not reject merely because the server returns a 404 or 500; check response.ok. A caller can pass an AbortController signal when a screen unmounts or a newer request supersedes an older one. Do not show cancellation as a user-facing server failure.
A component's responsibilities
Set loading before starting. On success, distinguish an empty array from an error. On failure, keep a clear retry action and avoid replacing useful existing data unnecessarily. Prevent an older search response from overwriting a newer query's result by cancelling it or checking a request identity.
CORS is a browser access policy
When frontend and API origins differ, the API must deliberately allow the intended origin, methods and headers. Credentialed cross-origin requests need compatible client and server settings; a wildcard origin is not valid for credentialed access. Do not “fix CORS” by disabling browser security or exposing the API to every origin. A same-origin development proxy can simplify local routing but does not define production authorization.
Validate writes on the server
Frontend validation improves feedback, while server validation enforces rules. A user can call the API without your UI. Derive user/tenant identity from trusted authentication context, not an arbitrary body field. Parameterize database queries and return only fields the interface needs.
Practice
- Display a loading message, a course list, an empty state and a retryable error.
- Rapidly change a search term and verify stale responses do not win.
- Return HTML instead of JSON in a development test and ensure the client reports a contract failure.
- Document allowed origins separately from per-user permission checks.