> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hr-easy.nlead.ch/llms.txt
> Use this file to discover all available pages before exploring further.

# Conventions

> Response envelopes, error shapes, pagination, idempotency, and versioning.

## Response envelope

Successful responses carry a literal `success: true` alongside the payload:

```json theme={null}
{
  "success": true,
  "data": {/* the DTO */}
}
```

The `success` field is typed as the literal `true` in the schema rather than as a
boolean, so a generated client can discriminate on it without a runtime check.

## Errors

Errors return a plain object with a message:

```json theme={null}
{ "error": "Unauthorized" }
```

Validation failures add the field-level detail:

```json theme={null}
{
  "error": "Validation failed",
  "details": {
    "fieldErrors": { "startDate": ["Invalid date"] },
    "formErrors": []
  }
}
```

Two rules the server holds itself to, which clients can rely on:

* **Routes never throw.** An unhandled condition becomes a structured error response,
  not a stack trace or an HTML error page.
* **Raw database objects are never returned.** Every response is projected to a DTO, so
  internal fields, tokens and unrelated columns cannot leak through an over-broad
  select.

## DTOs, not entities

Response shapes are named DTOs — `EmployeeOverviewDTO`, `PayslipDTO`,
`CompensationViewDTO` — generated from the Zod output schemas. They are a deliberate
projection of the domain model, and they are the compatibility surface. The database
schema behind them can change without breaking a client, and does.

## Result size

The portal contract is self-scoped — every response covers one person — so its list
endpoints take no `limit` parameter and return no pagination block today. Do not build a
client that expects one; if pagination is introduced it will arrive as **added** fields,
per the versioning rule below.

Underneath, results are bounded regardless: every `findMany` in an API route must carry
a `take` or a `cursor`, enforced in CI rather than left to review. The platform's
internal list endpoints — the ones behind the dashboard, not this contract — clamp a
`limit` parameter and return a `pagination: { total, limit, offset, hasMore }` block.

## Path parameter validation

Path parameters are constrained in the schema, not only in the handler. A payslip
period, for example, is validated against `^\d{4}-\d{2}$` and a malformed value returns
`400` with a message naming the expected `YYYY-MM` shape.

## Idempotency

The platform supports an `Idempotency-Key` header on **cost-bearing** operations — the
ones that move money or produce an irreversible external side effect, such as creating a
payment batch. A retry after a network blip replays the stored response instead of
producing a second batch. Keys are scoped per route so the same key from two endpoints
cannot collide, and are retained long enough to cover any realistic retry window.

<Note>
  No endpoint in the portal contract is cost-bearing in that sense, so the header has no effect
  here. It is documented because it is the platform-wide convention, and because a future portal
  write that does carry an external side effect will honour it.
</Note>

## Locale

Content is returned in the caller's locale where the content is translatable. Clients
do not need to request a language — the session carries it.

## Versioning

The employee endpoints are versioned in their path (`/api/v1/portal/…`). The two session
endpoints sit at `/api/auth/…` and are not part of that versioning. Within a version:

* Fields may be **added** to a response.
* Fields are not removed and their types do not change.
* New endpoints may appear.

A breaking change means a new version, not an in-place edit.

<Note>
  The specification is the contract. It is generated from the same schemas the server validates
  against and checked byte-for-byte in CI, so anything documented here is enforced at runtime rather
  than asserted in prose.
</Note>
