Fill gaps in shared pattern contracts

- modal-workflows: full state machine, htmx pattern, validation rules
- go-api: REST conventions, URL naming, status codes, error format, list response
- import-export: streaming export 3-layer architecture with Go example
- CLAUDE.template.md: updated to include modals and REST API references

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-01 16:47:52 +03:00
parent 40d1c303bb
commit a37aec8790
4 changed files with 231 additions and 50 deletions
+91
View File
@@ -0,0 +1,91 @@
# Contract: REST API Conventions (Go Web Applications)
## URL Naming
- Resources are plural nouns: `/api/assets`, `/api/components`, `/api/pricelists`.
- Nested resources use parent path: `/api/assets/:id/components`.
- Actions that are not CRUD use a verb suffix: `/api/pricelists/:id/export-csv`,
`/api/tasks/:id/cancel`, `/api/sync/push`.
- No verbs in resource paths: use `/api/assets` + DELETE, not `/api/delete-asset`.
## HTTP Methods and Status Codes
| Operation | Method | Success | Notes |
|-----------|--------|---------|-------|
| List | GET | 200 | |
| Get one | GET | 200 / 404 | |
| Create | POST | 201 | Return created resource |
| Update | PUT / PATCH | 200 | Return updated resource |
| Delete / Archive | DELETE | 200 or 204 | |
| Async action | POST | 202 | Return `{task_id}` |
| Validation error | POST/PUT | 422 | Return field errors |
| Server error | any | 500 | Log full error server-side |
| Not found | any | 404 | |
| Unauthorized | any | 401 | |
- Never return `200 OK` for validation errors — use `422 Unprocessable Entity`.
- Never return `200 OK` with `{"error": "..."}` in the body — use the correct status code.
## Error Response Format
All non-2xx responses return a consistent JSON body:
```json
{
"error": "human-readable message",
"fields": {
"serial_number": "Serial number is required",
"price": "Must be greater than 0"
}
}
```
- `error` — always present, describes what went wrong.
- `fields` — optional, present only for validation errors (422). Keys match form field names.
- Never expose raw Go error strings, stack traces, or SQL errors to the client.
## List Response Format
```json
{
"items": [...],
"total_count": 342,
"page": 2,
"per_page": 50,
"total_pages": 7
}
```
- Always include pagination metadata even if the client did not request it.
- `items` is always an array — never `null`, use `[]` for empty.
## Request Conventions
- Accept `application/json` for API endpoints.
- HTML form submissions (htmx) use `application/x-www-form-urlencoded` or `multipart/form-data`.
- File uploads use `multipart/form-data`.
- Query parameters for filtering and pagination: `?page=1&per_page=50&search=abc&status=active`.
## Health and Utility Endpoints
Every application must expose:
```
GET /health → 200 {"status": "ok"}
GET /api/db-status → 200 {"ok": true} or 500 {"ok": false, "error": "..."}
```
- `/health` must respond even if DB is down (for load balancer probes).
- `/api/db-status` checks the actual DB connection and returns its state.
## Async Actions
For long-running operations return immediately with a task reference:
```
POST /api/pricelists/create → 202 {"task_id": "abc123"}
GET /api/tasks/abc123 → 200 {"status": "running", "progress": 42, "message": "Processing..."}
GET /api/tasks/abc123 → 200 {"status": "success", "result": {...}}
```
See `go-background-tasks/contract.md` for full task contract.
+46 -1
View File
@@ -71,8 +71,53 @@ fmt.Sprintf("%.2f", price)
- Escape embedded double-quotes by doubling them: `""`.
- Use `encoding/csv` with `csv.Writer` and set `csv.Writer.Comma = ';'`; it handles quoting automatically.
## Streaming Export Architecture (Go)
For exports with potentially large row counts use a 3-layer streaming pattern.
Never load all rows into memory before writing — stream directly to the response writer.
```
Handler → sets HTTP headers + writes BOM → calls Service
Service → delegates to Repository with a row callback
Repository → queries in batches → calls callback per row
Handler/Service → csv.Writer.Flush() after all rows
```
```go
// Handler
func ExportCSV(c *gin.Context) {
c.Header("Content-Type", "text/csv; charset=utf-8")
c.Header("Content-Disposition", `attachment; filename="export.csv"`)
c.Writer.Write([]byte{0xEF, 0xBB, 0xBF}) // BOM
w := csv.NewWriter(c.Writer)
w.Comma = ';'
w.Write([]string{"ID", "Name", "Price"}) // header row
err := svc.StreamRows(ctx, filters, func(row Row) error {
return w.Write([]string{row.ID, row.Name, formatPrice(row.Price)})
})
w.Flush()
if err != nil {
// headers already sent — log only, cannot change status
slog.Error("csv export failed mid-stream", "err", err)
}
}
// Repository — batch fetch with callback
func (r *Repo) StreamRows(ctx, filters, fn func(Row) error) error {
rows, err := r.db.QueryContext(ctx, query, args...)
// ... scan and call fn(row) for each row
}
```
- Use `JOIN` in the repository query to avoid N+1 per row.
- Batch size is optional; streaming row-by-row is fine for most datasets.
- Always call `w.Flush()` after the loop — `csv.Writer` buffers internally.
## Error Handling
- Import errors should map to clear user-facing messages.
- Export errors after streaming starts must degrade gracefully (human-readable fallback).
- Export errors after streaming starts must be logged server-side only — HTTP headers are already
sent and the status code cannot be changed mid-stream.
+52 -5
View File
@@ -1,9 +1,56 @@
# Contract: Modal Workflows
## Shared Rules
## State Machine
- Destructive actions require explicit confirmation.
- Validation and backend errors are rendered in human-readable form.
- UI state transitions are explicit (`open` / `submit` / `success` / `error` / `cancel`).
- API contracts and UI copy should be documented in the host project's Bible.
Every modal has exactly these states:
```
closed → open → submitting → success | error
cancel → closed
```
- `open`: form visible, submit enabled.
- `submitting`: form disabled, spinner on submit button, no double-submit possible.
- `success`: close modal, show toast, refresh affected data.
- `error`: stay open, show error message inline, re-enable form.
- `cancel`: close without changes, no confirmation needed unless form is dirty.
## Rules
- Destructive actions (delete, archive, bulk remove) require a separate confirmation modal — not
just a disabled button. Confirmation modal must name the target: "Delete component ABC-123?"
- Never close a modal automatically on error — keep it open so the user can retry or copy the error.
- Submit button text must describe the action: "Save", "Delete", "Archive" — not "OK" or "Confirm".
- Modal title must match the action: "Edit Component", "Delete Asset" — not generic "Modal".
- Escape key and clicking the backdrop close the modal (unless in `submitting` state).
## Validation
- Validate on submit server-side. Client-side validation is optional progressive enhancement only.
- Show field-level errors inline below each field.
- Show a form-level error summary at the top if multiple fields fail.
- Error messages must be human-readable and action-oriented: "Serial number is required" — not
"serial_number: cannot be null".
## htmx Pattern (server-rendered modals)
```
POST /api/entity → 200 OK + HX-Trigger: "entitySaved" (success)
→ 422 Unprocessable + partial HTML (validation error, re-render form)
→ 500 + error message (server error)
```
- On success: server sends `HX-Trigger` header, JS listener closes modal and refreshes list.
- On validation error: server re-renders the form partial with inline errors (422).
- On server error: show generic error toast, log full error server-side.
- Do not use `200 OK` for validation errors — use `422` so htmx can differentiate.
## Multi-Step Modals
Use only when the workflow genuinely requires staged input (e.g. import preview → confirm).
- Show a step indicator (Step 1 of 3).
- Back button must restore previous step values.
- Final confirm step must summarise what will happen before the destructive/irreversible action.
- Single-step edits must NOT be split into multi-step without good reason.