This document is the frontend-facing single source of truth for consuming the Lieferscheine backend APIs.
Scope:
apiClient helper layer (lib/frontend/apiClient.js).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.jsinstead of hardcoding path strings.
Non-goals:
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:
credentials: "include"cache: "no-store"The project provides a tiny wrapper to enforce those defaults: lib/frontend/apiClient.js.
The core UI flow is a simple drill-down:
login({ username, password })getMe() (optional but recommended)getBranches()getYears(branch)getMonths(branch, year)getDays(branch, year, month)getFiles(branch, year, month, day)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;
}
}
File:
lib/frontend/routes.jsPurpose:
Example:
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");
apiClient helperFile:
lib/frontend/apiClient.jsDesign goals:
credentials: "include" and cache: "no-store".ApiClientError.ApiClientErrorWhen the backend returns the standardized error payload:
{ "error": { "message": "...", "code": "...", "details": {} } }
…the client throws:
name = "ApiClientError"status (HTTP status)code (machine-readable error code)message (safe human-readable message)detailsAuth:
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)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.mjsFor server checks, run it inside the app container:
docker compose exec app node scripts/manual-api-client-flow.mjs \
--baseUrl=http://127.0.0.1:3000 \
--username=<user> \
--password=<pw> \
--branch=NL01
branch: NL01, NL02, ...year: "YYYY" (4 digits)month: "MM" (2 digits, 01–12)day: "DD" (2 digits, 01–31)Current server responses are sorted ascending:
NL01, NL02, ...)If the UI needs “newest first”, reverse the arrays in the UI.
getFiles() returns:
{
"files": [{ "name": "...pdf", "relativePath": "NL01/2025/12/19/...pdf" }]
}
Notes:
relativePath is relative to NAS_ROOT_PATH inside the container.All routes are served under /api.
POST /api/auth/loginBody:
{ "username": "example.user", "password": "plain" }
Success:
{ "ok": true }
Errors:
400 VALIDATION_*401 AUTH_INVALID_CREDENTIALSGET /api/auth/logoutSuccess:
{ "ok": true }
GET /api/auth/meSuccess (authenticated):
{ "user": { "userId": "...", "role": "branch|admin|dev", "branchId": "NL01" } }
Success (unauthenticated):
{ "user": null }
All endpoints below require a valid session.
GET /api/branchesSuccess:
{ "branches": ["NL01", "NL02"] }
RBAC:
branch role: returns only its own branchadmin/dev: returns all branchesGET /api/branches/:branch/yearsSuccess:
{ "branch": "NL01", "years": ["2024", "2025"] }
GET /api/branches/:branch/:year/monthsSuccess:
{ "branch": "NL01", "year": "2025", "months": ["01", "02"] }
GET /api/branches/:branch/:year/:month/daysSuccess:
{ "branch": "NL01", "year": "2025", "month": "12", "days": ["18", "19"] }
GET /api/files?branch=&year=&month=&day=Success:
{
"branch": "NL01",
"year": "2025",
"month": "12",
"day": "19",
"files": [{ "name": "test.pdf", "relativePath": "NL01/2025/12/19/test.pdf" }]
}
GET /api/healthAlways returns 200 and reports partial system state:
All error responses use:
{
"error": {
"message": "Human readable message",
"code": "SOME_MACHINE_CODE",
"details": {}
}
}
Auth:
AUTH_UNAUTHENTICATEDAUTH_INVALID_CREDENTIALSAUTH_FORBIDDEN_BRANCHValidation:
VALIDATION_MISSING_PARAMVALIDATION_MISSING_QUERYVALIDATION_INVALID_JSONVALIDATION_INVALID_BODYVALIDATION_MISSING_FIELDStorage:
FS_NOT_FOUNDFS_STORAGE_ERRORInternal:
INTERNAL_SERVER_ERRORThe backend reads from a NAS where new scans can appear at any time.
Backend rules:
dynamic = "force-dynamic".Cache-Control: no-store.A small process-local TTL cache exists in lib/storage.js:
Frontend guidance:
credentials: "include" and cache: "no-store".The repository contains a manual smoke test script that exercises:
Script:
scripts/manual-api-client-flow.mjsLocal:
node scripts/manual-api-client-flow.mjs \
--baseUrl=http://localhost:3000 \
--username=<user> \
--password=<pw> \
--branch=NL01
Server (recommended from within container):
docker compose exec app node scripts/manual-api-client-flow.mjs \
--baseUrl=http://127.0.0.1:3000 \
--username=<user> \
--password=<pw> \
--branch=NL01
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:
If a breaking change becomes necessary, introduce a new endpoint rather than modifying the existing contract.
PDF delivery (download/stream) is not part of the current v1 surface documented above.
Planned as additive change: