frontend-api-usage.md 14 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.
  • 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)

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.
  • 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:

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

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:

import { buildPdfUrl } from "@/lib/frontend/explorer/pdfUrl";

const href = buildPdfUrl({ branch, year, month, day, filename });

// In JSX:
// <a href={href} target="_blank" rel="noopener noreferrer">Öffnen</a>

Force download:

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:

{
	"items": [
		{
			"branch": "NL20",
			"date": "2025-12-18",
			"year": "2025",
			"month": "12",
			"day": "18",
			"filename": "...pdf",
			"relativePath": "NL20/2025/12/18/...pdf",
			"snippet": "..."
		}
	],
	"nextCursor": "<opaque>"
}

Recommended usage (client-side):

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:

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

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

  • 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)