Skip to content
Slicekit

Frontend

The typed API client

How the frontend talks to the API: one typed client, cookies and CSRF handled for you, wrapped in TanStack Query.

View .md
On this page

One client, one contract

Every call from the SPA to the API goes through a single typed client. Centralising it means cookies, CSRF, error handling and base URLs are configured in exactly one place; features just call typed functions.

// features/projects/api.ts
import { api } from '@/lib/api'

export const listProjects = () => api.get<ProjectDto[]>('/projects')
export const createProject = (body: CreateProject) => api.post<{ id: string }>('/projects', body)

Cookies and CSRF

The client sends requests with credentials so the session cookie is included automatically. For state-changing requests it attaches the CSRF token the API expects. None of this is the feature author’s concern: call api.post and the protection is applied.

Errors

The API returns a consistent error shape, and the client surfaces it as a typed error. Error codes map to translation keys (snake_case), so the UI can show a localized message without scattering string literals through components.

try {
  await createProject({ name })
} catch (err) {
  // err.code -> 'project_name_taken' -> localized message
}

Wrapped in TanStack Query

The raw client functions are wrapped in query and mutation hooks per feature. Components depend on the hooks, not the transport:

export function useCreateProject() {
  const qc = useQueryClient()
  return useMutation({
    mutationFn: createProject,
    onSuccess: () => qc.invalidateQueries({ queryKey: ['projects'] }),
  })
}

Keeping types honest

The request and response types are hand-written to mirror the API’s shapes; there is no codegen step (see adding a frontend feature). Centralising the client keeps every call site consistent, but keeping those types in line with the API is a manual discipline: rename a field on the server and the TypeScript still compiles, so the interactive API reference at /scalar on the running API is the source of truth for every endpoint. If you want the wire enforced at build time, generate the types from the published OpenAPI document (for example with openapi-typescript) and import them instead of hand-writing them.