These docs are a work in progress and may not be fully up to date. Some pages may contain internal notes for our team.
Skip to Content

Next.js — API layer

All backend communication in the Next.js app goes through two wrapper functions. Direct calls to fetch() are prohibited by convention (AGENTS.md). The wrappers handle JWT auth, audit headers, workspace scoping, and error surfacing uniformly.

Core wrappers (src/lib/api/core.ts)

fetchAPI<T>(endpoint, options?)

Calls ${NEXT_PUBLIC_API_URL}${endpoint}. Used for all main dashboard API calls.

fetchExt<T>(endpoint, options?)

Calls ${NEXT_PUBLIC_API_URL}ext/${endpoint}. Used for widget-facing ext/ calls made from the dashboard (e.g., publish, sync).

Both delegate to the internal fetchJson<T> function which:

  1. Reads the JWT from useAuthStore.getState().token (Zustand + localStorage persistence).
  2. Attaches Authorization: Bearer <token> if a token exists.
  3. Sets Content-Type: application/json for non-FormData bodies.
  4. Calls buildAuditHeaders() if the method is POST, PUT, PATCH, or DELETE.
  5. Throws on non-2xx. On 401, logs out and redirects to /login.
  6. Returns {} on 204 No Content.

AuditRequestOptions

Extends RequestInit with four optional fields that map to the audit header system:

FieldHeader sent
auditActionX-Audit-Action
auditResourceTypeX-Audit-Resource-Type
auditResourceIdX-Audit-Resource-Id
auditRequestIdX-Audit-Request-Id (defaults to crypto.randomUUID())

Audit headers are only sent on mutating methods. X-Audit-Request-Id is always set on mutations even if the other fields are omitted.

buildWorkspaceAwareEndpoint(endpoint, workspaceId?)

Appends ?workspace_id=<id> (or &workspace_id=<id>) to an endpoint. If workspaceId is not passed, reads useWorkspaceStore.getState().activeWorkspaceId. If no workspace is active, returns the endpoint unchanged.

Most resource API calls in domain modules call this before passing the endpoint to fetchAPI.

getErrorMessageFromData(errorData, fallback)

Extracts a human-readable message from the error response JSON. Priority: messageerror → first value in errors → first array value in any other key → fallback string. API modules pass the result to addAlert(message, 'error') from useUiStore.

Domain modules (src/lib/api/*.ts)

Each resource has its own file. All are thin wrappers over fetchAPI / fetchExt:

FileResources covered
auth.tslogin, register, me, logout
finders.tsfinder CRUD, analytics, integrations
locations.tslocation CRUD, image upload, CSV import
maps.tsmap CRUD
sets.tsset/category CRUD
tags.tstag CRUD
customFields.tscustom field definition CRUD
workspaces.tsworkspace CRUD, member management
aiImports.tsAI location import job lifecycle
aiImprovements.tsAI location improvement job lifecycle
processes.tsprocess polling and SSE
admin.tsadmin-only operations

Normalizers and payloads

normalizers.ts

Converts raw API response shapes to typed domain objects. All useQuery results pass through a normalizer before being consumed by components. This is the boundary where snake_case API fields become camelCase TypeScript types.

payloads.ts

Converts domain values to the wire format expected by the API. Used by mutation calls before sending. Handles JSON serialization of complex fields (e.g., design config, settings array).