# Conventions


## API path conventions

All HTTP routes in microservices follow standardized path conventions so
the BE's MCP Server can dispatch generic CRUD tools without per-entity
configuration.

**Route handlers are factory-produced (MANDATORY):** the service's whole
HTTP surface is declared ONCE via the SDK's `makeServiceRoutes` — entity
declarations become `makeEntityRouter` + `entityCrudSpec`, `RpcRoute[]`
tables become `makeRpcRouter` + `rpcSpec`, and `/api/v1/openapi.json`
via `makeOpenApiHandler` — all derived from that single declaration, so
the published spec can never drift from the registered routes.
`createMicroservice` refuses to boot with a hand-rolled
`(req, res, url) => boolean` handler — auth, RBAC, body validation and
RFC 7807 errors are enforced by the factory, not by convention.

### Entity CRUD (`/api/v1/entities/:entity/...`)

Database-backed entities with standard CRUD lifecycle use this pattern:

```
GET    /api/v1/entities/:entity/meta              → entity metadata (fields, types, validation)
GET    /api/v1/entities/:entity/list              → paginated list with search/filter/sort
GET    /api/v1/entities/:entity/:uuid             → single record by UUID
POST   /api/v1/entities/:entity                   → create new record
PUT    /api/v1/entities/:entity/:uuid             → update record by UUID
DELETE /api/v1/entities/:entity/:uuid             → soft-delete record by UUID
POST   /api/v1/entities/:entity/:uuid/restore     → restore soft-deleted record
GET    /api/v1/entities/:entity/:uuid/audit       → audit history for record
POST   /api/v1/entities/:entity/bulk-delete       → bulk soft-delete (array of UUIDs)
POST   /api/v1/entities/:entity/bulk-restore      → bulk restore (array of UUIDs)
```

Rules:
- `:entity` is the snake_case **singular** noun (e.g. `provider`,
  `config_entry`). Never plural, never camelCase.
- `:uuid` is always the UUID path parameter.
- Not all entities support all operations — unsupported operations simply
  don't register that route.
- The `meta` endpoint returns the entity's field schema, consumed by the
  MCP `get_entity_meta` tool and the FE for dynamic form generation.
- **Write-payload standard (`{entity}` envelope):** single-entity
  `POST`/`PUT` bodies MUST be `{ entity: {...} }` — flat entity fields at
  the body root are rejected with `400` (hard break, same contract as the
  BE). A `translations` sibling is NOT accepted on microservice entities
  (no translations table in the service schema). Bulk endpoints
  (`bulk-*`), actions, `DELETE` and RPC routes keep their own bodies.
  The MCP proxy dispatch already wraps tool args into `{ entity }` — no
  per-tool change needed.

Example — list providers through the BE proxy:

```bash
# List with search + pagination
curl -H "Authorization: Bearer <jwt>" \
  "http://localhost:3001/ws/emailsender/api/v1/entities/provider/list?search=brevo&page=1&page_size=20"

# Get entity metadata (field schema for dynamic forms)
curl -H "Authorization: Bearer <jwt>" \
  http://localhost:3001/ws/emailsender/api/v1/entities/provider/meta

# Create a provider ({entity} envelope)
curl -X POST -H "Authorization: Bearer <jwt>" -H "Content-Type: application/json" \
  http://localhost:3001/ws/emailsender/api/v1/entities/provider \
  -d '{"entity":{"provider":"brevo","api_key":"xkeysib-...","from_email":"noreply@example.com"}}'

# Soft-delete by UUID
curl -X DELETE -H "Authorization: Bearer <jwt>" \
  http://localhost:3001/ws/emailsender/api/v1/entities/provider/550e8400-e29b-41d4-a716-446655440000

# Restore a soft-deleted record
curl -X POST -H "Authorization: Bearer <jwt>" \
  http://localhost:3001/ws/emailsender/api/v1/entities/provider/550e8400-e29b-41d4-a716-446655440000/restore
```

### Service actions (`/api/v1/actions/:action`)

Non-CRUD business actions:

```
POST   /api/v1/actions/send-email                 → send an email using a template
POST   /api/v1/actions/test-provider-connection   → test a provider's API key
```

- `:action` is a snake_case verb-noun.
- These are NOT exposed as MCP generic CRUD tools.

### Webhooks (`/webhook` or `/webhook/:identifier`)

External callbacks (e.g. Brevo™ delivery events):

```
POST   /webhook                                    → webhook receiver (API key auth)
POST   /webhook/brevo                              → Brevo-specific webhook
```

- Webhooks use API key authentication, NOT JWT.
- Webhook paths do NOT use the `/api/v1/` prefix.

### System / Health

```
GET    /health                                     → health check (public, no auth)
GET    /api/v1/openapi.json                        → OpenAPI spec (client-shielded — the BE fetches it via internalFetch during discovery)
GET    /api/v1/system/info                         → service info (authenticated)
```

### OpenAPI spec requirements

Every microservice MUST export a complete OpenAPI 3.x spec at
`GET /api/v1/openapi.json`:

1. List ALL implemented routes
2. Use `operationId` in snake_case for every operation
3. Include `tags` grouping operations by entity or category
4. Include `summary` and `description` for every operation
5. Use `snake_case` field names in request/response schemas

## Data model rules

### Naming convention

| Layer | Name form | Example |
|---|---|---|
| PG table | snake_case **plural** | `providers`, `config_entries`, `sender_logs` |
| TS entity class | PascalCase **singular** | `ProviderEntity`, `ConfigEntryEntity` |
| API URL entity segment | snake_case **singular** | `/api/v1/entities/provider/...` |

- Entity class = singular (one row schema)
- PG table = plural (collection of rows)
- URL entity segment = singular (resource type)

### Snake_case everywhere

DB columns, TS interfaces, JSON request bodies, and JSON response bodies
ALL use `snake_case`. A field named `from_email` in the DB is
`from_email` in the TS interface and `from_email` in the JSON response.
Never rename fields between layers.

**Exception:** External API adapters (e.g. Brevo™ expecting camelCase).
The translation happens ONLY at the adapter boundary.

### No DTO transformation

The DB row IS the TS model. Do not create intermediate DTO classes that
rename fields. Prefer spreading raw results (`return { ...settings }`)
over field-by-field rebuilding.

### No fake defaults on the read path

- Forbidden: lowercasing, uppercasing, trimming on the read path
- Forbidden: fallback string-literal defaults (`|| "..."`, `?? ""`) for
  configuration data
- A value either exists in the DB or it doesn't — `undefined` if missing,
  `null` if the row exists but value is NULL, `string` if present
- Mandatory-field checks throw before the return, they don't fake a default

### Type conversions are allowed

- `string` (DB) → `boolean` (TS) via `=== "true"` — allowed
- `string` (DB) → `enum` (TS) via validation + normalization — allowed

These are type conversions, not data-quality enforcement. Data quality is
enforced at the write path (API upsert validation).

## Package versioning

All package versions in `package.json` MUST be pinned to exact versions
(e.g. `"typescript": "5.9.3"`). NO ranges (`^`, `~`, `>=`, `*`, `latest`)
are allowed for registry packages. This ensures every dev machine, CI
build, and production rebuild gets the exact same dependency tree.

Workspace dependencies use `workspace:*` (e.g.
`"@primebrick/sdk": "workspace:*"`).

## Repo tooling

| Tool | Where | Purpose |
|---|---|---|
| `scripts/version-sync.mjs` | repo root | `version:auto`/`prebuild` hook — keeps `version` aligned across service package.jsons |
| `<svc>/scripts/database-patch-apply.ts` | per service | `db:migrate` — runs the dal-pg `runMigrations` engine against the service's own schema (same code path as boot) |
| `<svc>/scripts/build-docs-pack.ts` | per service | `generate:docs` — builds `dist/docs-pack` (SDK `buildDocsPack`) |

## Next steps

- [Architecture](/microservices/dev/architecture) — NATS™ bus, BE proxy, SDK lifecycle
- [EmailSender](/microservices/dev/services/emailsender) — Email sending microservice
