frontend-api-usage.md 9.0 KB

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.

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.
  • PDF streaming/viewer implementation details (see “Out of scope / planned”).

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)

1.3 Example usage (client-side)

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:

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:

{ "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:

docker compose exec app node scripts/manual-api-client-flow.mjs \
  --baseUrl=http://127.0.0.1:3000 \
  --username=<user> \
  --password=<pw> \
  --branch=NL01

3. API v1 conventions

3.1 Identifiers

  • branch: NL01, NL02, ...
  • year: "YYYY" (4 digits)
  • month: "MM" (2 digits, 0112)
  • day: "DD" (2 digits, 0131)

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:

{
	"files": [{ "name": "...pdf", "relativePath": "NL01/2025/12/19/...pdf" }]
}

Notes:

  • relativePath is relative to NAS_ROOT_PATH inside the container.
  • Treat it as an opaque identifier for a future download/stream endpoint.

4. Endpoint contracts (v1)

All routes are served under /api.

4.1 Auth

POST /api/auth/login

Body:

{ "username": "example.user", "password": "plain" }

Success:

{ "ok": true }

Errors:

  • 400 VALIDATION_*
  • 401 AUTH_INVALID_CREDENTIALS

GET /api/auth/logout

Success:

{ "ok": true }

GET /api/auth/me

Success (authenticated):

{ "user": { "userId": "...", "role": "branch|admin|dev", "branchId": "NL01" } }

Success (unauthenticated):

{ "user": null }

4.2 Branch navigation

All endpoints below require a valid session.

GET /api/branches

Success:

{ "branches": ["NL01", "NL02"] }

RBAC:

  • branch role: returns only its own branch
  • admin/dev: returns all branches

GET /api/branches/:branch/years

Success:

{ "branch": "NL01", "years": ["2024", "2025"] }

GET /api/branches/:branch/:year/months

Success:

{ "branch": "NL01", "year": "2025", "months": ["01", "02"] }

GET /api/branches/:branch/:year/:month/days

Success:

{ "branch": "NL01", "year": "2025", "month": "12", "days": ["18", "19"] }

4.3 Files

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" }]
}

4.4 Health

GET /api/health

Always returns 200 and reports partial system state:

  • database connectivity
  • NAS readability

5. Error handling

5.1 Standard error payload

All error responses use:

{
	"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:

  • happy path drill-down
  • negative cases (401/403/400/404)

Script:

  • scripts/manual-api-client-flow.mjs

Local:

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

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

PDF delivery (download/stream) is not part of the current v1 surface documented above.

Planned as additive change:

  • a dedicated endpoint to stream or download a PDF while enforcing RBAC server-side.