frontend-api-usage.md 17 KB

Frontend API Usage (v1)

This document is the frontend-facing single source of truth for consuming the RHL 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 UI.
  • PDF streaming/opening behavior in the Explorer and in Search.
  • Search date filters (from / to) and shareable URL sync (RHL-025).

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).
  • The Search UI integrates the same “Open PDF” pattern (RHL-024).
  • The Search UI supports optional date filters (from / to) with a date range picker and presets (RHL-025).

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 Explorer 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 cross-branch search UI (see section 4.4)
  • apply date filters via from / to in YYYY-MM-DD format (RHL-025)

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)

Search:

  • search({ q, branch, scope, branches, from, to, limit, cursor })

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 (and Search results 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.

In the frontend, prefer the dedicated wrapper:

  • apiClient.search(...)

Query params:

  • q (optional)

Optional filters:

  • scope: branch | all | multi
  • branch: single branch
  • branches: comma-separated branch list (for scope=multi)
  • from, to: YYYY-MM-DD (inclusive)
  • limit: page size (default 100, allowed 50..200)
  • cursor: pagination cursor returned by the previous response

Filter rule:

  • The backend requires at least one filter to avoid accidental “match everything” searches:
    • q OR from OR to

If all three are missing, the API returns:

  • 400 VALIDATION_SEARCH_MISSING_FILTER

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>",
	"total": 123
}

Notes:

  • nextCursor is null when there are no more results.
  • total is the total number of matches for the current query and can be shown as “x of y loaded”.
  • total may be null if the provider cannot provide a reliable total.
4.4.1 Date filters (RHL-025)

The Search UI supports optional date filtering:

  • from / to are inclusive ISO date strings (YYYY-MM-DD).
  • from === to is valid (single-day filter).
  • from > to is invalid and must be rejected.

Frontend responsibilities:

  • Validate locally for fast feedback (UI should not send invalid date ranges).
  • Treat backend validation as authoritative (backend may still return VALIDATION_SEARCH_DATE / VALIDATION_SEARCH_RANGE).

Relevant frontend helpers:

  • lib/frontend/search/dateRange.js (pure ISO date helpers + German formatting)
  • lib/frontend/search/searchDateValidation.js (canonical date-range validation)
  • lib/frontend/search/dateFilterValidation.js (builds a local ApiClientError for UI rendering)
4.4.2 URL sync (shareable)

For Search v1, the first-page identity is URL-driven (shareable).

  • q, scope, branches, limit, from, to are part of the shareable state.
  • cursor is intentionally not part of the shareable URL and stays in client state.
4.4.3 Recommended usage (client-side)
import { search, ApiClientError } from "@/lib/frontend/apiClient";

export async function searchDeliveryNotesExample() {
	try {
		const res = await search({
			q: "bridgestone",
			branch: "NL20",
			from: "2025-12-01",
			to: "2025-12-31",
			limit: 100,
		});

		return {
			items: res.items,
			nextCursor: res.nextCursor,
			total: res.total,
		};
	} catch (err) {
		if (err instanceof ApiClientError) {
			// Example:
			// - AUTH_UNAUTHENTICATED -> redirect to login
			// - AUTH_FORBIDDEN_BRANCH -> show forbidden
			// - VALIDATION_* -> show a friendly input message
		}
		throw err;
	}
}

Date-range-only example (no q):

  • The API allows this as long as at least one of from / to is provided.
  • The current v1 UI intentionally requires q to trigger a search to avoid accidental broad queries.

    import { search } from "@/lib/frontend/apiClient";
    
    export async function searchByDateRangeOnly() {
    	const res = await search({
    		scope: "all",
    		from: "2025-12-01",
    		to: "2025-12-31",
    		limit: 100,
    	});
    
    	return res;
    }
    

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

Search validation:

  • VALIDATION_SEARCH_SCOPE
  • VALIDATION_SEARCH_BRANCH
  • VALIDATION_SEARCH_BRANCHES
  • VALIDATION_SEARCH_DATE
  • VALIDATION_SEARCH_RANGE
  • VALIDATION_SEARCH_LIMIT
  • VALIDATION_SEARCH_CURSOR
  • VALIDATION_SEARCH_MISSING_FILTER

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

  • Date-only Search UI mode (admin/dev):

    • The API supports searching by date range without q.
    • The current v1 UI intentionally requires q to trigger a search.
    • A dedicated UI toggle (“Search by date only”) could be added later to enable this safely.
  • Optional Search UX improvements:

    • grouping results by date / branch
    • debounced search (optional; current v1 is explicit submit)
  • Optional Explorer UX polish:

    • add a dedicated “Herunterladen” UI action (download variant)
    • optional in-app PDF viewer experience (instead of a new tab) ich habe zu hause einen pc zum programmieren aber auch zum zocken. ich frage mich wie teuer es wird wenn ich meine grafikkarte so update dass ich AAA-Spiele auf 4k spielen kann

derzeit habe ich eine RTX4060ti eingebaut. derzeit spiele ich auch viel auf der ps5 aber ich moechte in zukunft komplett umsteigen auf pc.

was brauche ich um spiele wie eafc, nba2k, anno, gta etc auf 4k mit hoher frame zu spielen?