# Frontend API Usage (v1) This document is the **frontend-facing** single source of truth for consuming the Lieferscheine backend APIs. Scope: - Stable **API v1** contracts (URLs, params, response shapes). - The minimal frontend `apiClient` helper layer (`lib/frontend/apiClient.js`). - Practical examples for building the first UI. - PDF streaming/opening behavior in the Explorer (RHL-023). > UI developers: For the app shell layout and frontend route scaffold (public vs protected routes, placeholder pages), see **`Docs/frontend-ui.md`**. > > For UI navigation, prefer centralized route builders from `lib/frontend/routes.js` instead of hardcoding path strings. Non-goals: - New major features. - An in-app PDF viewer UI (beyond opening the browser’s PDF viewer in a new tab). Notes: - The backend provides a binary PDF stream/download endpoint (RHL-015). The Explorer integrates it for “Open PDF” (RHL-023). --- ## 1. Quickstart ### 1.1 Recommended calling pattern For the first UI, prefer calling the backend from **browser/client-side code** so the HTTP-only session cookie is naturally sent with requests. Key fetch requirements: - Always send cookies: `credentials: "include"` - Always request fresh data: `cache: "no-store"` The project provides a tiny wrapper to enforce those defaults: `lib/frontend/apiClient.js`. ### 1.2 Happy-path navigation flow The core UI flow is a simple drill-down: 1. `login({ username, password })` 2. `getMe()` (optional but recommended) 3. `getBranches()` 4. `getYears(branch)` 5. `getMonths(branch, year)` 6. `getDays(branch, year, month)` 7. `getFiles(branch, year, month, day)` 8. Open a PDF via the binary endpoint (see section 4.3) Optional (admin/dev): - use the search endpoint (`GET /api/search`) to implement a global search UI (see section 4.4) ### 1.3 Example usage (client-side) ```js import { login, getMe, getBranches, getYears, getMonths, getDays, getFiles, ApiClientError, } from "@/lib/frontend/apiClient"; export async function runExampleFlow() { try { await login({ username: "nl01user", password: "secret" }); const me = await getMe(); if (!me.user) throw new Error("Expected to be logged in"); const { branches } = await getBranches(); const branch = branches[0]; const { years } = await getYears(branch); const year = years[years.length - 1]; const { months } = await getMonths(branch, year); const month = months[months.length - 1]; const { days } = await getDays(branch, year, month); const day = days[days.length - 1]; const { files } = await getFiles(branch, year, month, day); return { branch, year, month, day, files }; } catch (err) { if (err instanceof ApiClientError) { // Use err.code for UI decisions. // Example: AUTH_UNAUTHENTICATED -> redirect to login throw err; } throw err; } } ``` ### 1.4 Frontend route helpers (UI navigation) File: - `lib/frontend/routes.js` Purpose: - Centralize URL building so UI code does not scatter hardcoded strings. - Encode dynamic segments defensively. Example: ```js import { branchPath, dayPath, searchPath, loginPath, } from "@/lib/frontend/routes"; const loginUrl = loginPath(); const branchUrl = branchPath("NL01"); const dayUrl = dayPath("NL01", "2025", "12", "31"); const searchUrl = searchPath("NL01"); ``` --- ## 2. The `apiClient` helper File: - `lib/frontend/apiClient.js` Design goals: - Enforce `credentials: "include"` and `cache: "no-store"`. - Parse JSON automatically. - Convert standardized backend errors into a single error type: `ApiClientError`. ### 2.1 `ApiClientError` When the backend returns the standardized error payload: ```json { "error": { "message": "...", "code": "...", "details": {} } } ``` …the client throws: - `name = "ApiClientError"` - `status` (HTTP status) - `code` (machine-readable error code) - `message` (safe human-readable message) - optional `details` ### 2.2 Provided helpers Auth: - `login({ username, password })` - `logout()` - `getMe()` Navigation: - `getBranches()` - `getYears(branch)` - `getMonths(branch, year)` - `getDays(branch, year, month)` Files: - `getFiles(branch, year, month, day)` Low-level: - `apiFetch(path, options)` ### 2.3 Server-side usage note (Node / scripts) Node’s `fetch` does **not** include a cookie jar automatically. For manual verification we provide a script that includes a minimal cookie jar: - `scripts/manual-api-client-flow.mjs` For server checks, run it **inside the app container**: ```bash docker compose exec app node scripts/manual-api-client-flow.mjs \ --baseUrl=http://127.0.0.1:3000 \ --username= \ --password= \ --branch=NL01 ``` --- ## 3. API v1 conventions ### 3.1 Identifiers - `branch`: `NL01`, `NL02`, ... - `year`: `"YYYY"` (4 digits) - `month`: `"MM"` (2 digits, `01`–`12`) - `day`: `"DD"` (2 digits, `01`–`31`) ### 3.2 Sorting Current server responses are sorted **ascending**: - branches: ascending by branch number (`NL01`, `NL02`, ...) - years/months/days: numeric ascending - files: lexicographic ascending by file name If the UI needs “newest first”, reverse the arrays in the UI. ### 3.3 File entries `getFiles()` returns: ```json { "files": [{ "name": "...pdf", "relativePath": "NL01/2025/12/19/...pdf" }] } ``` Notes: - `relativePath` is **relative to `NAS_ROOT_PATH`** inside the container. - The PDF stream endpoint uses **route segments** (`branch/year/month/day/filename`). - For v1 UI usage, treat `files[].name` as the canonical filename and build the stream URL from the current Explorer route segments. --- ## 4. Endpoint contracts (v1) All routes are served under `/api`. ### 4.1 Auth #### `POST /api/auth/login` Body: ```json { "username": "example.user", "password": "plain" } ``` Success: ```json { "ok": true } ``` Errors: - `400 VALIDATION_*` - `401 AUTH_INVALID_CREDENTIALS` #### `GET /api/auth/logout` Success: ```json { "ok": true } ``` #### `GET /api/auth/me` Success (authenticated): ```json { "user": { "userId": "...", "role": "branch|admin|dev", "branchId": "NL01" } } ``` Success (unauthenticated): ```json { "user": null } ``` ### 4.2 Branch navigation All endpoints below require a valid session. #### `GET /api/branches` Success: ```json { "branches": ["NL01", "NL02"] } ``` RBAC: - `branch` role: returns only its own branch - `admin`/`dev`: returns all branches #### `GET /api/branches/:branch/years` Success: ```json { "branch": "NL01", "years": ["2024", "2025"] } ``` #### `GET /api/branches/:branch/:year/months` Success: ```json { "branch": "NL01", "year": "2025", "months": ["01", "02"] } ``` #### `GET /api/branches/:branch/:year/:month/days` Success: ```json { "branch": "NL01", "year": "2025", "month": "12", "days": ["18", "19"] } ``` ### 4.3 Files #### `GET /api/files?branch=&year=&month=&day=` Success: ```json { "branch": "NL01", "year": "2025", "month": "12", "day": "19", "files": [{ "name": "test.pdf", "relativePath": "NL01/2025/12/19/test.pdf" }] } ``` #### `GET /api/files/:branch/:year/:month/:day/:filename` This endpoint returns **binary PDF data** on the happy path (not JSON). Frontend rules: - **Do not call this endpoint via `apiClient.apiFetch()`**. - `apiClient` is JSON-centric and will try to parse the response. - Prefer opening the endpoint URL in a **new tab** so the browser handles PDF rendering. ##### 4.3.1 Centralized URL builder File: - `lib/frontend/explorer/pdfUrl.js` Exports: - `buildPdfUrl({ branch, year, month, day, filename })` - `buildPdfDownloadUrl({ branch, year, month, day, filename })` (adds `?download=1`) Why it exists: - Keeps URL construction consistent. - Ensures the `filename` segment is encoded correctly. ##### 4.3.2 Recommended UI usage pattern In the Explorer table, we open PDFs via navigation: - Use a normal anchor element with `target="_blank"`. - This avoids popup-blocker issues and is semantically correct. Example: ```js import { buildPdfUrl } from "@/lib/frontend/explorer/pdfUrl"; const href = buildPdfUrl({ branch, year, month, day, filename }); // In JSX: // Öffnen ``` Force download: ```js import { buildPdfDownloadUrl } from "@/lib/frontend/explorer/pdfUrl"; const href = buildPdfDownloadUrl({ branch, year, month, day, filename }); ``` ##### 4.3.3 Important filename and cookie notes - Use the exact `files[].name` returned by `getFiles()` (case-sensitive on Linux). - Filenames with special characters must be URL-encoded. - In particular, `#` **must** be encoded as `%23`. Otherwise the browser treats it as a fragment and the server receives a truncated filename. - Host consistency matters for cookies: - If you are logged in on `http://localhost:3000`, also open the PDF on `http://localhost:3000`. - Switching to `http://127.0.0.1:3000` will not send the cookie (different host) and results in `401`. ### 4.4 Search #### `GET /api/search` This is a **JSON** endpoint. You can call it via `apiFetch(...)` (or add a small convenience wrapper in `apiClient` when implementing the Search UI). Query params: - `q` (required) Optional filters: - `scope`: `branch | all | multi` - `branch`: single branch - `branches`: comma-separated branch list (for `scope=multi`) - `from`, `to`: `YYYY-MM-DD` - `limit`: page size - `cursor`: pagination cursor returned by the previous response Response shape: ```json { "items": [ { "branch": "NL20", "date": "2025-12-18", "year": "2025", "month": "12", "day": "18", "filename": "...pdf", "relativePath": "NL20/2025/12/18/...pdf", "snippet": "..." } ], "nextCursor": "" } ``` Recommended usage (client-side): ```js import { apiFetch } from "@/lib/frontend/apiClient"; export async function searchDeliveryNotes({ q, scope, branch, branches, from, to, limit, cursor, }) { const params = new URLSearchParams(); params.set("q", q); if (scope) params.set("scope", scope); if (branch) params.set("branch", branch); if (branches?.length) params.set("branches", branches.join(",")); if (from) params.set("from", from); if (to) params.set("to", to); if (limit) params.set("limit", String(limit)); if (cursor) params.set("cursor", cursor); return apiFetch(`/api/search?${params.toString()}`); } ``` Using a hit to navigate/open: - Navigate to the day folder: - `dayPath(hit.branch, hit.year, hit.month, hit.day)` - Open the PDF: - `buildPdfUrl({ branch: hit.branch, year: hit.year, month: hit.month, day: hit.day, filename: hit.filename })` Pagination: - Store `nextCursor` from the response. - Pass it back as `cursor=` for the next page. - Treat the cursor as opaque. RBAC note: - Branch users will only get results for their own branch. - Admin/dev users can use `scope=all` or `scope=multi` for cross-branch search. --- ## 5. Error handling ### 5.1 Standard error payload All JSON error responses use: ```json { "error": { "message": "Human readable message", "code": "SOME_MACHINE_CODE", "details": {} } } ``` ### 5.2 Common codes used by the UI Auth: - `AUTH_UNAUTHENTICATED` - `AUTH_INVALID_CREDENTIALS` - `AUTH_FORBIDDEN_BRANCH` Validation: - `VALIDATION_MISSING_PARAM` - `VALIDATION_MISSING_QUERY` - `VALIDATION_INVALID_JSON` - `VALIDATION_INVALID_BODY` - `VALIDATION_MISSING_FIELD` Storage: - `FS_NOT_FOUND` - `FS_STORAGE_ERROR` Internal: - `INTERNAL_SERVER_ERROR` --- ## 6. Caching & freshness The backend reads from a NAS where new scans can appear at any time. Backend rules: - All route handlers are `dynamic = "force-dynamic"`. - All JSON responses include `Cache-Control: no-store`. - A small process-local TTL cache exists in `lib/storage.js`: - branches/years: 60s - months/days/files: 15s Frontend guidance: - Use `credentials: "include"` and `cache: "no-store"`. - Do not rely on Next.js ISR/revalidate for these endpoints. --- ## 7. Manual verification (RHL-008) The repository contains a manual smoke test script that exercises: - authentication - drill-down navigation (branches -> years -> months -> days -> files) - negative cases (401/403/400/404) Script: - `scripts/manual-api-client-flow.mjs` Local: ```bash node scripts/manual-api-client-flow.mjs \ --baseUrl=http://localhost:3000 \ --username= \ --password= \ --branch=NL01 ``` Server (recommended from within container): ```bash docker compose exec app node scripts/manual-api-client-flow.mjs \ --baseUrl=http://127.0.0.1:3000 \ --username= \ --password= \ --branch=NL01 ``` --- ## 8. API v1 freeze policy As of RHL-008, the endpoints and response shapes documented here are considered **API v1**. Rules: - Avoid breaking changes to existing URLs, parameters, or response fields. - Prefer additive changes: - add new endpoints - add optional fields - If a breaking change becomes necessary, introduce a new endpoint rather than modifying the existing contract. --- ## 9. Out of scope / planned additions - Search UI and filters (route exists as placeholder: `/:branch/search`). - Backend support exists via `GET /api/search`. - The UI is still considered a planned addition. - Optional Explorer UX polish: - add a dedicated “Herunterladen” UI action (download variant) - optional in-app PDF viewer experience (instead of a new tab)