API core
The SPA talks to the Workers API through one shared client in apps/web/src/lib/services/api/core.ts. Domain modules (usersService, maintenanceService, …) call request() — they do not create their own Axios instances.
Related: API services · Guides: API · Query.
What it is
| Piece | Role |
|---|---|
apiClient | Axios instance: baseURL, JSON headers, withCredentials: true |
| Response interceptor | On HTTP 401 (and not already on /login), redirect to /login |
request<T>(config) | Typed wrapper: unwraps success data, throws ApiError on failure |
ApiError | Carries code, status, parsed field errors / messages for UI |
Base URL comes from apps/web/src/lib/config.ts:
- Dev default:
http://localhost:8787 - Prod default:
https://api.cepatedge.com - Override:
VITE_API_BASE_URL
Cookies (session JWT / session id) are sent cross-subdomain when domains are configured — see Domains.
Workers response envelope
The Worker returns a consistent JSON shape (ApiResponseBody in apps/web/src/types/api/):
Success
{
"success": true,
"code": "OK",
"data": { "...": "payload" },
"message": "optional"
}Error
{
"success": false,
"code": "VALIDATION_ERROR",
"error": { "message": "...", "details": {}, "issues": [] },
"message": "optional"
}request<T>():
- Calls
apiClient.request - If
success === false→ buildApiErrorand throw - If
success === true→ returnbody.dataasT(unwrapped) - Network / non-envelope failures →
ApiErrorwithINTERNAL_ERROR
So callers always get data or throw — never a mixed { success, data } object.
request() options
Beyond normal Axios config (method, url, params, data, …):
| Option | Default | Meaning |
|---|---|---|
handleErrors | true | Run handleApiError() (toasts / global UX) before rethrowing |
context | — | String passed into the error handler for logging / copy |
Set handleErrors: false when a page wants to render the error inline without a global toast.
Standard call pattern
import { request } from '@/lib/services/api/core';
// Inside a domain service — not inside a React page
export async function getUsers(params?: UsersListParams): Promise<UsersListResult> {
return request<UsersListResult>({
method: 'GET',
url: '/users',
params: /* mapped query */,
});
}Pages and query hooks call services, not request() directly. That keeps transport, typing, and URL mapping in one place.
Error codes (surface)
ResponseCode includes values such as OK, VALIDATION_ERROR, UNAUTHORIZED, FORBIDDEN, ERR_PERMISSION, NOT_FOUND, RATE_LIMIT_EXCEEDED, SESSION_EXPIRED, attachment limits, and INTERNAL_ERROR. Prefer branching on error.code, not on free-text messages.
Maintainer rules
- New HTTP calls go through
request()+ a domain service underlib/services/<domain>/. - Do not add a second Axios instance for “just this feature.”
- Keep cookie auth (
withCredentials) — do not switch to bearer headers in the SPA without an ADR. - Align types with the Worker OpenAPI / Zod contracts; do not invent a parallel response shape.