# Frontend (`web/`) — Agent Instructions
React 19 + daisyUI 5 (Tailwind v4) on Bun. Routing with wouter, server state with `useAsync`,
native `<form>` and `<dialog>`. The backend's OpenAPI spec is the contract between the two halves.
## Layers
```
presentation → infrastructure → application → domain
```
| `src/domain/<entity>/` | types, the error (`code` = the API's `ErrorResponse.name`), the messages for those codes, the repository port | imports an npm package or another layer |
| `src/application/` | use cases — **only when there is logic** (see its README) | imports an adapter or a screen |
| `src/infrastructure/` | adapters implementing the domain ports (HTTP) | imports a screen or a use case |
| `src/presentation/` | pages, containers (state + calls), views (props only), shared UI | imports `infrastructure/` (only `core/generated/dependencies.ts` does) |
`bun run lint:arch` (dependency-cruiser) fails on any import that breaks this.
Screens reach adapters through `useDependencies()`, never by importing them: a test renders a
screen with `renderScreen(<Screen />, { productRepository: fake })` (`src/test-support/render.tsx`)
and needs no network.
## An entity's slice
`puerto generate scaffold <Name>` writes, next to the Rust layers, the screens of a CRUD entity
(`puerto generate web <Name>` writes them alone, for an entity already in `puerto.toml`):
```
src/domain/<entity>/ model.ts · errors.ts · messages.ts · repository.ts (the port)
src/infrastructure/<entity>/ <entity>-repository-http.ts (+ test): DTO ↔ model, ErrorResponse → error
src/presentation/features/<entity>/
containers/ list · detail · form (create + edit): state and repository calls (+ tests)
views/ list · detail · form: props only
src/test-support/<entity>.ts <Entity>Mother + fake<Entity>Repository (in memory)
```
Form inputs hold text; `src/presentation/shared/forms.ts` converts it to the model and back
(`null` for an empty optional input, comma-separated lists, `key=value` lines, `datetime-local`
in local time). An enum value object is a union type with a `parse…` function in `model.ts`.
Lists are paged: the port's `list(page)` answers a `Page<T>` (`src/domain/shared/page.ts` —
`items`, `total`, the `limit` the API applied, `offset`); the list container keeps the offset and
renders `shared/ui/pagination.tsx`. The API caps `limit` at 100 whatever the screen asks.
Routes: `/<plural>`, `/<plural>/new`, `/<plural>/:id`, `/<plural>/:id/edit`. Containers call the
repository port directly; write a use case in `src/application/` once there is logic to hold.
`src/test-support/` is for tests: it never reaches an adapter, and only test files import it.
## Generated files
- **Error messages** live in `src/domain/<entity>/messages.ts`, generated from the backend's error
list: `toProblem(code)` answers the text and the form field it is about. The form shows it under
that input (`FieldError`, `aria-invalid`), anything else in the alert; an unknown code shows as
it is. English — that file is where to translate (a forced re-scaffold rewrites it).
- **Never hand-edit `src/presentation/core/generated/`** — `dependencies.ts` (the composition
root) and `routes.tsx` (the route table) are rewritten from `puerto.toml` by
`puerto generate scaffold`, `puerto generate web` and `puerto generate bootstrap`. A screen of
your own gets its route in **`src/presentation/core/routes.tsx`** (`customRoutes`, each
`<Route>` with a `key`): yours, matched before the generated routes, never rewritten.
- **Never hand-edit `src/infrastructure/http/schema.d.ts`** — the wire types, generated by
`openapi-typescript` from `../docs/api/openapi.json`. **`make web/sync`** (project root)
re-exports the spec from the Rust code, regenerates them and formats `web/` with Biome; run it
after a scaffold and after any DTO or route change. Until then `bun run typecheck` fails where
the frontend no longer matches the API.
- Everything else is yours once written — `src/infrastructure/http/client.ts` included, written
with the first entity.
## Conventions
- **No client-side copy of domain validation.** The server is the authority: show the error code
it answers (`product.validation_error.name_empty`) with `ErrorAlert`. HTML `required` / `type`
only.
- **TDD with `bun test`** (happy-dom + Testing Library). Test names say what the user sees:
`should show the error code when the error carries one`, with `// Arrange` / `// Act` /
`// Assert`.
- Styling is daisyUI classes (`btn`, `card`, `alert`, `modal`); no component library.
## Check before you are done
```bash
make web/test # bun test
make web/lint # tsc + Biome + dependency-cruiser
```
`make test` and `make lint` at the project root run these too.