Skip to content

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

PieceRole
apiClientAxios instance: baseURL, JSON headers, withCredentials: true
Response interceptorOn HTTP 401 (and not already on /login), redirect to /login
request<T>(config)Typed wrapper: unwraps success data, throws ApiError on failure
ApiErrorCarries 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

json
{
  "success": true,
  "code": "OK",
  "data": { "...": "payload" },
  "message": "optional"
}

Error

json
{
  "success": false,
  "code": "VALIDATION_ERROR",
  "error": { "message": "...", "details": {}, "issues": [] },
  "message": "optional"
}

request<T>():

  1. Calls apiClient.request
  2. If success === false → build ApiError and throw
  3. If success === true → return body.data as T (unwrapped)
  4. Network / non-envelope failures → ApiError with INTERNAL_ERROR

So callers always get data or throw — never a mixed { success, data } object.


request() options

Beyond normal Axios config (method, url, params, data, …):

OptionDefaultMeaning
handleErrorstrueRun handleApiError() (toasts / global UX) before rethrowing
contextString 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

typescript
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

  1. New HTTP calls go through request() + a domain service under lib/services/<domain>/.
  2. Do not add a second Axios instance for “just this feature.”
  3. Keep cookie auth (withCredentials) — do not switch to bearer headers in the SPA without an ADR.
  4. Align types with the Worker OpenAPI / Zod contracts; do not invent a parallel response shape.