Files
SlpSoftware/aidlc-docs/features/react-frontend/construction/react-frontend-app/functional-design/packages-api-handoff.md
T
Sluijsens 99abf5dff0
Continuous Integration / config (pull_request) Successful in 9s
Continuous Integration / prepare (pull_request) Successful in 1m14s
Continuous Integration / build-production (pull_request) Skipped
Continuous Integration / build (pull_request) Successful in 2m1s
Continuous Integration / test (pull_request) Successful in 2m7s
Continuous Integration / deploy-production (pull_request) Skipped
Deploy / deploy (pull_request) Successful in 35s
Continuous Integration / deploy-test (pull_request) Successful in 36s
Hernoem packages endpoint naar /api/v1/offerings
De frontend haalt de pakketten-data nu op via /api/v1/offerings i.p.v.
/api/v1/packages.
2026-08-01 22:20:31 +02:00

6.4 KiB

Hand-off: Packages API (GET /api/v1/offerings)

Audience: whoever implements the CMS/backend that will serve package content. Status: frontend is already wired up and waiting on this endpoint to exist.

Context

The "Drie manieren om te starten" section on the marketing site (PackagesSection.tsx) used to render hardcoded package cards. It now fetches them from an API instead, via usePackagesQuery.ts, so package content (title, price, features, ...) can be managed in a CMS without a frontend deploy.

Until this endpoint exists, the section will show the error state ("Pakketten konden niet worden geladen. Probeer het later opnieuw.") in any environment that doesn't have it yet.

What the frontend needs

Endpoint

GET /api/v1/offerings
  • Same domain as the site, relative path. The frontend calls fetch('/api/v1/offerings') with no protocol/host — it relies on the API being reachable under the same origin the React app is served from, both on test.slpsoftware.nl and on production (slpsoftware.nl). Today, both Pis only serve the static React build (see aidlc-docs/features/react-frontend/operations/deployment/nginx/*.conf.example — there is no /api/ location anywhere yet). Whoever builds the backend also needs to add an nginx location /api/v1/ { proxy_pass ... } block (reverse-proxy Pi and/or webserver Pi, wherever the backend process actually runs) for both the test and production nginx configs — the frontend change alone does not create that route.
  • Method: GET only. No mutation endpoints are needed by this frontend (CMS authoring is out of scope for the marketing site itself).
  • Auth: none — this is public marketing content, same as the rest of the page.
  • CORS: not needed if served same-origin as above. If the backend ends up on a different origin instead, CORS must be enabled for test.slpsoftware.nl and slpsoftware.nl and the frontend's relative URL would need to become absolute (not the current plan, but flagging the coupling).

Response contract

200 OK, Content-Type: application/json, body is a JSON array of package objects, in display order (the array order is rendered left-to-right as-is — no client-side sorting):

[
  {
    "id": "pakket_01",
    "title": "Landingspagina",
    "description": "Eén overtuigende pagina die je product of dienst helder neerzet.",
    "price": "€ 300",
    "priceNote": "eenmalig, excl. btw",
    "features": [
      "Eén pagina in HTML & CSS",
      "Ontwerp op maat, geen template",
      "Responsive op elk scherm",
      "Snelle laadtijd & SEO-basis"
    ],
    "ctaLabel": "Kies landingspagina",
    "featured": false
  }
]

Any non-2xx response (or a network error) is treated by the frontend as a failure — it shows a generic error message and logs nothing card-specific, so there's no need for a structured error body.

Field reference

Field Type Required Content type / description Used for
id string Yes Stable slug-like identifier, e.g. pakket_01. Currently just an ordinal slug, not a business meaning — reuse as the CMS entry's own key/slug. React list key + data-testid on the card, and referenced in tests. Must be unique and stable across edits (don't regenerate on every content change, or it'll be treated as a different card).
title string Yes Short package name (e.g. "Landingspagina"). Plain text, no markup. Card heading.
description string Yes One-sentence pitch, plain text. Subheading under the title.
price string Yes Pre-formatted price display string — e.g. "€ 300" or "Op maat" for the quote-based package. Not a number: it's rendered verbatim, so formatting (currency symbol, spacing, "Op maat") is the CMS editor's responsibility, not computed client-side. Large price figure on the card.
priceNote string Yes Small print under the price, e.g. "eenmalig, excl. btw" or "offerte na intake". Plain text. Caption under the price.
features string[] Yes Ordered bullet list of what's included, plain text per item (no markup). Typically 3-4 items. Rendered as a -bulleted feature list; array order = display order.
ctaLabel string Yes Button text, e.g. "Kies landingspagina" or "Vraag offerte aan". Call-to-action button label (the button itself always links to the contact section — the API does not control the link target).
featured boolean No (defaults to falsy if omitted) Marks the "most chosen" package. Exactly one package in the list should have this true — the frontend doesn't enforce or dedupe this, it will just visually highlight and badge every package where it's true. Adds the "Meest gekozen" badge and a visually distinct card style.

This mirrors the existing PackageCard domain entity documented in domain-entities.md — that table is the authoritative field list; this doc adds the API/content-type framing on top of it.

Seed / reference content

The three packages currently hardcoded in content.ts (pakket_01 Landingspagina, pakket_02 Website, pakket_03 Maatwerk) are the real, currently-live copy (per business rule BR-4, content fidelity matters — see business-rules.md). Use that file as the seed data / source of truth when populating the CMS for the first time, so the site's content doesn't change the moment this endpoint goes live.

Caching behaviour on the frontend (informational)

The frontend uses TanStack Query with default settings (query key ['packages'], no custom staleTime) — it refetches on every mount/window-refocus rather than caching indefinitely. No Cache-Control/ETag support is required from the API for correctness, but adding reasonable HTTP caching is welcome since this content changes rarely.

Open items for the API implementer

  • Add the /api/v1/ nginx proxy location for both test and production (see "Same domain" note above) — without this, the relative fetch has nothing to reach even once the backend exists.
  • Decide/confirm hosting for the backend + CMS (not decided by this hand-off).
  • Seed initial content from content.ts (see "Seed / reference content" above).