Documentation & API Contracts
1. Purpose
This document describes how the Foodtruck Order repository documents itself, which tools are used and why, what counts as a public API that must be documented, and how the generated documentation is produced, checked, and published.
The documentation system has four layers:
Markdown documentation -> human-written project/architecture/development docs (docs/)
OpenAPI -> the authoritative contract for the HTTP API (docs/api/openapi.yaml)
Swagger UI -> an interactive rendering of the OpenAPI document
TypeDoc -> generated TypeScript reference for intentional public APIs
A single command rebuilds the whole site from a clean checkout. Blocking checks detect invalid contracts, missing configured public API comments, and generation errors. Review checks comment meaning and documentation accuracy; the operational merge policy requires passing CI.
2. Conceptual separation
The layers must not be confused:
- OpenAPI is the contract.
docs/api/openapi.yamlis the authoritative, version-controlled description of every intentionally public HTTP endpoint. It is independent of the HTTP framework (currently Fastify) and of any presentation tooling. - Swagger UI is only its presentation layer. It renders the committed OpenAPI document interactively. If Swagger UI disagrees with the YAML file, the YAML file is the contract and the mistake is in the presentation.
- TypeDoc is the reference documentation for public TypeScript APIs. It is generated by the TypeScript compiler's own type system, not by hand.
- Markdown documentation (
docs/) is the source of truth for architecture, development workflow, testing, CI, and Docker. The generated site renders this Markdown rather than duplicating it.
3. Tools and why
- Redocly validates the version-controlled OpenAPI 3.1 contract.
- Swagger UI renders that contract; it is not generated from a live backend.
- TypeDoc generates intentional public TypeScript reference and checks configured comment/export/link coverage through the compiler's type system.
- A small Node script using marked renders existing Markdown into the static site; this avoids a second source of documentation/navigation in a separate framework.
- A Node static preview server serves generated files locally over HTTP.
Explicitly not used: Doxygen, Sphinx, and any tool that would parse TypeScript with regexes.
4. What is a public API
For the purposes of this repository, the documented public API is the set of symbols exported from
the TypeDoc entry points configured in typedoc.json. The entry points intentionally cover one
boundary per application area:
packages/db/src/index.ts— the entire public surface of the@foodtruck-order/dbpackage (the package's declaredexportsentry point).backend/src/...— the backend's intentional public API:buildApp(app.ts), the error contract (api-errors.ts), the application services (message-service.ts,user-service.ts,password-service.ts,product-service.ts), the domain types (message.ts,user.ts,product.ts), and the repository contracts (message-repository.ts,user-repository.ts,product-repository.ts).frontend/src/...— the frontend's intentionally public API boundary, split into the same architectural areas the code itself uses:- the API/service boundary:
app/api/api-client.ts(transport, error contract, base-URL resolution),app/api/message-api.ts, andapp/api/product-api.ts(feature-facing endpoint operations); - application state and platform modules:
app/preferences/*,app/i18n/*,app/notifications/notification-context.tsx,app/query-client.ts,app/router.tsx,app/pages.tsx,app/providers.tsx,app/theme.ts,app/layout/AppLayout.tsx; - build/runtime diagnostics:
app/build-metadata.ts,app/build-metadata-values.ts,app/identity.ts,app/diagnostics.ts,app/errors/error-messages.ts; - reusable presentation components:
components/async-states.tsx,components/error-boundary.tsx,components/validation-feedback.tsx; and the composition rootApp.tsx.
- the API/service boundary:
Everything else is implementation detail, even if it is technically exported:
- the backend's database-specific implementations (
repository/postgres-*.ts), - the backend's process bootstrap (
server.ts) and its configuration module (config.ts), - the frontend's Vite bootstrap (
main.tsx), individual page implementations (pages/*.tsx), and internal rendering details of documented components, - private helpers, local functions and variables, test code, generated code, declaration files that are generated, configuration files, and third-party type definitions.
When a new package or a new intentional cross-package boundary appears, its entry point is added to
typedoc.json and its public symbols become subject to the documentation gate.
5. Which symbols require inline documentation
Every symbol exported from a documented entry point requires a meaningful inline TSDoc
(/** ... */) comment:
- exported functions and their intentional public behavior;
- exported classes (and their public methods and constructors);
- exported interfaces, type aliases, and enums;
- exported constants/variables that form part of the public API;
- public class/interface members — properties in particular when their semantics are not completely obvious.
The comment must explain semantics, not repeat the name. Prefer useful @param, @returns, and
@throws tags where they convey information that the types do not; do not add tags mechanically. A
placeholder comment such as /** Creates an order. */ for a function that has non-obvious behavior
is a violation.
No documentation is required for private symbols, local functions/variables, implementation details, generated code, test-only code, configuration files, generated declaration files, or third-party dependencies. Missing documentation must be fixed by writing real documentation, never by weakening the gate or adding placeholder text.
6. Documentation checks
The canonical commands:
pnpm docs:check # OpenAPI validation + TypeDoc documentation-coverage gate
pnpm docs:openapi:check # redocly lint docs/api/openapi.yaml (redocly.yaml)
pnpm docs:typedoc:check # TypeDoc generation with strict validation (same as pnpm docs:typedoc)
pnpm docs:links:check # tracked Markdown files, images, targets, and heading fragments
pnpm spellcheck # scoped authored Markdown, code comments, and UI resources
pnpm docs:check is part of the repository-wide static quality gate (pnpm quality), so every CI
quality run and every local pnpm quality executes it.
- The OpenAPI check fails when
docs/api/openapi.yamlis not valid OpenAPI 3.1 or violates the rules inredocly.yaml. - The TypeDoc check fails when a configured public symbol lacks a documentation comment. Comment
meaning and completeness require human review; TypeDoc does not judge prose semantics. It is
implemented by TypeDoc's own validation (AST/type-aware), runs with
treatValidationWarningsAsErrors, and reports the offending package/file/symbol, for example:backend/src/app.BuildAppOptions (Interface), defined in @foodtruck-order/backend/src/app.ts, does not have any documentation.
All checks are blocking quality gates, not informational reports. The local-link checker lexes
Markdown with marked, so fenced examples are not mistaken for links. Relative targets resolve from
their source file and heading fragments use the same Unicode/duplicate-heading algorithm as the site
generator. External HTTP availability is deliberately excluded from offline quality and runs
weekly/manually as pnpm docs:links:external; provider or network failure remains visible there.
CSpell uses pinned English, Swedish, and Hungarian dictionaries plus a small project vocabulary.
Generated/vendor output, the lockfile, environment files, and credential previews are excluded;
authored specifications are not broadly ignored.
The external checker is committed repository code using the workflow's pinned Node 24 built-in HTTP client, rather than an unpinned downloaded CLI. It has one narrow URL-family exemption: this private repository's four GitHub Actions workflow page/badge links return an unauthenticated 404 even when valid. Their workflow files are checked locally and their hosted conclusions remain part of CI evidence; no other GitHub or HTTP target is exempt.
7. Generating the documentation site
pnpm docs:build # clean + Swagger UI + TypeDoc + Markdown site (complete static site)
pnpm docs:api # Swagger UI page (docs-site/api/)
pnpm docs:typedoc # TypeDoc reference (docs-site/reference/)
pnpm docs:site # Markdown pages + landing page (docs-site/, docs-site/project/, ...)
pnpm docs:site:check # required outputs plus generated local link/fragment validation
pnpm docs:serve # serve docs-site/ locally at http://127.0.0.1:4175
pnpm docs:build is deterministic and works from a clean checkout: it starts by deleting
docs-site/, then regenerates the Swagger UI page, the TypeDoc reference, and the Markdown pages.
The generated site is never committed (docs-site/ is gitignored); CI and the publishing workflow
always regenerate it.
After generation, docs:site:check requires non-empty landing, project, API, TypeDoc reference, raw
README, and AZ-013 source outputs, then checks every generated local href/src and fragment. Raw
.md fragments are validated against Markdown headings rather than treated as HTML element IDs.
The resulting docs-site/ contains:
docs-site/
index.html # landing page linking all three sections
project/ # rendered Markdown documentation
decisions/ # optional, created when docs/decisions/ contains Markdown records
raw/ # raw Markdown sources (so .md links keep working)
api/ # Swagger UI + openapi.yaml
reference/ # TypeDoc TypeScript reference
docs-site/project/ renders the project guides, the epic specifications and their task
specifications in docs/specs/, and the records in docs/decisions/; the rendered page list,
sidebar groups, and slugs are defined by the projectPages manifest in
scripts/docs-site-manifest.mjs. Each entry declares one committed source file (relative to the
repository root), the generated slug, the sidebar title that mirrors the document's own H1, and
the sidebar group. scripts/build-docs-site.mjs owns only reading those sources and writing the
site tree, while scripts/docs-site-render.mjs owns Markdown rendering, .md link rewriting, and
the shared HTML template; both consume the manifest, so a single declarative list drives generation
and the documentation checks.
To add a new canonical guide or specification index, add exactly one manifest entry and commit the
Markdown it points at — no generator code changes and no URL changes to existing pages are needed.
Individual task records are intentionally not listed: as with Epic 003, an epic renders only its
index and leaves its per-task Markdown as raw-linked copies under docs-site/raw/, so their stable
relative .md links keep resolving without touching the manifest. Markdown that is not rendered —
for example the forwarding pointer docs/specs/definition-of-done.md — is still copied to raw/,
so relative .md links keep resolving to a committed source file.
Standalone requirements live in docs/specs/standalone/, with one NNN.md file per stable
STANDALONE-NNN requirement and a README index. Their detailed Given/When/Then scenarios use IDs
such as STANDALONE-001-S01; implementation tests carry those exact IDs and the requirement's
implementation record links the evidence and validation. The standalone index is the canonical human
entry point, so it is included once in the documentation-site manifest; individual requirement files
remain raw-linked copies, following the same index/detail pattern as epics.
8. How to update the API contract
OpenAPI is a source document, not generated output. When a backend HTTP endpoint changes:
- Update
docs/api/openapi.yamlin the same change that changes the route: path, method, parameters, request body, response status codes, response schemas, headers, and error responses. Reuse/update the sharedcomponentsinstead of duplicating definitions. - Run
pnpm docs:openapi:check(and, via the backend unit suite,pnpm test) to validate the document. - The automated contract test (
backend/src/openapi-contract.test.ts) checks that the documentation and the implementation agree on the route surface: every Fastify route (excluding the implicit FastifyHEADroute) must have a matching documented path and operation, and every documented path and operation must exist as a Fastify route. - Bump
info.versionin the OpenAPI document for breaking contract changes.
The contract test intentionally verifies the route surface, not the implementation; do not duplicate the route table elsewhere.
9. Cloudflare Pages publishing
Documentation is published to Cloudflare Pages by the dedicated workflow
.github/workflows/cloudflare-pages.yml, which runs on pushes to main (and on manual
workflow_dispatch):
- Check out the repository.
- Install dependencies with the repository's pinned pnpm/Node strategy
(
pnpm install --frozen-lockfile). - Run the required quality checks:
pnpm quality(which includesdocs:check— OpenAPI validation and the documentation-coverage gate) andpnpm test. - Build the complete site with
pnpm docs:build. - Verify required output and generated links with
pnpm docs:site:check. - Upload
docs-site/to the already-existing Cloudflare Pages projectfoodtruck-order-docs(Direct Upload) with wrangler, run viapnpm dlxwith the version pinned in the workflow (pnpm dlx wrangler@4.131.1 pages deploy docs-site --project-name=foodtruck-order-docs --branch=main). No generated HTML is committed to the repository.
Authentication uses exactly the two repository secrets CLOUDFLARE_API_TOKEN (a least-privilege
token created for Pages deployment) and CLOUDFLARE_ACCOUNT_ID; neither value is hard-coded
anywhere in the repository, and the Cloudflare Global API Key is not used.
The division of responsibility is deliberate: GitHub hosts the source, runs the CI quality gates,
and generates the site; Cloudflare Pages only hosts the generated static output. Cloudflare is not
connected to GitHub (no Git integration, no Cloudflare-side build) and cannot clone the private
repository — the direct upload is the only path between the two. The published URL is the Pages
project's production domain (Cloudflare dashboard, Workers & Pages → foodtruck-order-docs);
--branch=main matches the workflow trigger and the project's production branch (the default for
Direct Upload projects), so automated deployments are production deployments.
Wrangler uploads the rebuilt site directly; it creates no GitHub Pages artifact and requires no
stale-artifact cleanup on workflow reruns. The workflow's only permission is contents: read;
nothing is uploaded when any step fails.
The normal CI pipeline (.github/workflows/ci.yml) already runs the same quality gates on every
pull request, and its docs job builds the site (pnpm docs:build), so broken TypeDoc/Swagger/site
generation fails PR validation. The operational merge policy requires passing CI; publishing
independently checks its own gates.
10. Deliberately excluded cases
- No production Swagger endpoint. Exposing Swagger UI from the running backend would add a dependency on the OpenAPI document at runtime for no benefit; the documentation site is static and hosted separately.
- No frontend package documentation. The frontend is an application, not a library; nothing in
this repository consumes its exports. If a shared frontend package is introduced later, its entry
point joins
typedoc.json. - No documentation for internal configuration (
backend/src/config.ts) and no documentation for the backend's database-specific repository implementation. They are exported only because they are used across the backend's own modules and tests, not because they form an intentional API. - Generated output is not committed, and the documentation site does not require any hosting
provider to work locally (
pnpm docs:build && pnpm docs:serve).
11. Source ownership and maintenance
Keep one authoritative home for each subject:
| Subject | Authoritative location | Maintenance rule |
|---|---|---|
| Architecture and dependency boundaries | architecture.md |
Explain responsibilities and rationale; link to commands elsewhere. |
| Technology choices and versions | techstack.md |
Record adopted/deferred technology, not feature status. |
| Setup and developer commands | development.md |
Keep runnable workflows here; link to policy and architecture. |
| Test levels, data, and coverage | test-strategy.md |
Own evidence selection and suite composition. |
| CI jobs, artifacts, and enforcement | ci.md |
Describe pipeline mechanics and enforcement gaps. |
| Containers and Compose | docker.md |
Own image, service, startup, and isolation behavior. |
| Documentation and API/reference generation | documentation.md |
Own placement, contract, generation, and publishing rules. |
| Completion requirements | definition-of-done.md |
State what must be evidenced; do not duplicate implementation mechanics. |
| Feature requirements and traceability | specs/ |
Preserve committed records and stable identifiers as history. |
| Architectural decisions | decisions/ |
Add or supersede records; never rewrite decision history. |
Cross-link instead of copying full commands or policies into multiple guides. A root guide should start with its purpose and document map before repository-specific detail. Reorder a living guide when incremental additions obscure that structure, but do not restructure documentation solely for visual uniformity.
current-state.md is the current implementation/validation/remaining-work snapshot.
ai-handover.md contains only the latest task and continuation notes, linking to that snapshot.
Keep neither as an accumulating changelog; historical execution records remain in Git. Informal
personal notes are not maintained project documentation; intentional product ideas belong in the
appropriate specification/backlog, while discarded notes remain recoverable from Git history.
Markdown targets 100 columns with Prettier proseWrap: always; see
development §13. Long unbreakable URLs, code, and
small tables are exceptions. Prefer sections/lists to wide prose tables.