33 - Add cookie-session API client for the dev proxy

This commit is contained in:
myrmidex 2026-06-28 02:32:39 +02:00
parent 28757d1e6b
commit 79367b3de0
2 changed files with 87 additions and 0 deletions

68
frontend/src/lib/api.ts Normal file
View file

@ -0,0 +1,68 @@
/**
* Thin API client for the Symfony + API Platform backend.
*
* Auth is cookie-session (#30): every request sends credentials so the session
* cookie set by POST /api/login flows on subsequent calls. In dev the Vite proxy
* keeps /api same-origin, so no CORS is involved. JSON-LD (Hydra) is the wire
* format; PATCH uses application/merge-patch+json per API Platform.
*/
const BASE = '/api'
/** Thrown for any non-2xx response; carries the status and raw body. */
export class ApiError extends Error {
readonly status: number
readonly body: string
constructor(status: number, body: string) {
super(`API request failed with status ${status}`)
this.name = 'ApiError'
this.status = status
this.body = body
}
}
async function request<T>(path: string, init: RequestInit = {}): Promise<T> {
const res = await fetch(`${BASE}${path}`, {
credentials: 'include',
...init,
headers: {
Accept: 'application/ld+json',
...init.headers,
},
})
if (!res.ok) {
throw new ApiError(res.status, await res.text())
}
// 204 No Content (e.g. DELETE) has an empty body — don't parse it as JSON.
// This cast assumes 204 only occurs where T is void (delete); the API does
// not 204 on GET/POST/PATCH, so a typed get<T> never reaches this path.
if (res.status === 204) {
return undefined as T
}
return res.json() as Promise<T>
}
export const api = {
get: <T>(path: string): Promise<T> => request<T>(path),
post: <T>(path: string, body: unknown): Promise<T> =>
request<T>(path, {
method: 'POST',
body: JSON.stringify(body),
headers: { 'Content-Type': 'application/ld+json' },
}),
patch: <T>(path: string, body: unknown): Promise<T> =>
request<T>(path, {
method: 'PATCH',
body: JSON.stringify(body),
headers: { 'Content-Type': 'application/merge-patch+json' },
}),
delete: (path: string): Promise<void> =>
request<void>(path, { method: 'DELETE' }),
}

19
frontend/src/types/api.ts Normal file
View file

@ -0,0 +1,19 @@
/**
* JSON-LD / Hydra envelope shapes shared by every API Platform resource.
* Domain models (Scenario/Bucket/Stream) are added by the feature tickets that
* consume them this file only types the transport envelope.
*/
/** Fields every API Platform item carries in a JSON-LD response. */
export interface HydraResource {
'@id': string
'@type': string
}
/** A Hydra collection response: items live under `member`. */
export interface HydraCollection<T> {
'@id': string
'@type': string
member: T[]
totalItems: number
}