Standardize API response envelope shapes across all endpoints
This commit standardizes how API responses are wrapped, solving issue #24. Problem: - Inconsistent response envelopes (jails vs items vs bans vs no wrapper) - Frontend required multiple field name variants - Integration bugs from branching logic - No clear pattern for different response types Solution: - Created response.py with base classes: PaginatedListResponse, CollectionResponse, CommandResponse - Standardized all list/collection responses to use 'items' field - Domain-specific field names for detail and aggregation responses - Updated all backends routers and mappers - Updated frontend types and hooks to match Changes: Backend: - backend/app/models/response.py (new): Base response models - backend/app/models/ban.py: Updated responses to inherit from bases - backend/app/models/jail.py: Updated JailListResponse, JailCommandResponse - backend/app/models/config.py: Updated collection responses - backend/app/services/jail_service.py: Updated return statements - backend/app/mappers/ban_mappers.py: Updated 'bans' to 'items' - backend/tests/test_mappers/test_ban_mappers.py: Updated tests Frontend: - frontend/src/types/jail.ts: Updated response interfaces - frontend/src/types/config.ts: Updated response interfaces - frontend/src/hooks/useActiveBans.ts: Updated selector - frontend/src/hooks/useJailList.ts: Updated selector - frontend/src/hooks/useJailConfigs.ts: Updated selector - frontend/src/hooks/useConfigActiveStatus.ts: Updated field access - frontend/src/hooks/useJailAdmin.ts: Updated field access Documentation: - Docs/Backend-Development.md: Added § 4.1 API Response Envelope Policy The policy defines: 1. Paginated lists use PaginatedListResponse (items, total, page, page_size) 2. Non-paginated collections use CollectionResponse (items, total) 3. Detail responses use entity-specific field names (jail, status, settings) 4. Command responses use CommandResponse (message, success, optional target) 5. Aggregations use domain-specific fields (jails, countries, buckets, bans) All responses now follow one of these patterns, reducing frontend complexity. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -310,6 +310,158 @@ async def login(
|
||||
|
||||
---
|
||||
|
||||
## 4.1 API Response Envelope Policy
|
||||
|
||||
All API responses must follow a **consistent wrapper pattern**. This standardization reduces frontend branching logic, prevents integration bugs, and makes the API easier to document and maintain.
|
||||
|
||||
### Response Patterns
|
||||
|
||||
#### Pattern 1: Paginated Lists
|
||||
|
||||
Use `PaginatedListResponse[T]` for endpoints that return paginated collections:
|
||||
|
||||
```python
|
||||
from app.models.response import PaginatedListResponse
|
||||
|
||||
class JailListResponse(PaginatedListResponse[JailSummary]):
|
||||
"""Response for ``GET /api/jails``."""
|
||||
pass
|
||||
|
||||
# Returns:
|
||||
{
|
||||
"items": [...], # T[]
|
||||
"total": 100, # int: total items across all pages
|
||||
"page": 2, # int: current page (1-based)
|
||||
"page_size": 20 # int: items per page
|
||||
}
|
||||
```
|
||||
|
||||
**When to use:** Endpoints that support pagination parameters (`page`, `page_size`, `limit`, `offset`).
|
||||
|
||||
#### Pattern 2: Non-Paginated Collections
|
||||
|
||||
Use `CollectionResponse[T]` for endpoints that return a complete collection without pagination:
|
||||
|
||||
```python
|
||||
from app.models.response import CollectionResponse
|
||||
|
||||
class JailConfigListResponse(CollectionResponse[JailConfig]):
|
||||
"""Response for ``GET /api/config/jails``."""
|
||||
pass
|
||||
|
||||
# Returns:
|
||||
{
|
||||
"items": [...], # T[]
|
||||
"total": 42 # int: total items
|
||||
}
|
||||
```
|
||||
|
||||
**When to use:** Endpoints that return a complete collection (not paginated). The frontend can render all items without worrying about paging.
|
||||
|
||||
#### Pattern 3: Single-Item Detail Responses
|
||||
|
||||
Use **domain-specific field names** (not generic wrappers) for detail endpoints:
|
||||
|
||||
```python
|
||||
class JailDetailResponse(BaseModel):
|
||||
"""Response for ``GET /api/jails/{name}``."""
|
||||
jail: Jail
|
||||
ignore_list: list[str]
|
||||
ignore_self: bool
|
||||
|
||||
# Returns:
|
||||
{
|
||||
"jail": { ... }, # Jail object
|
||||
"ignore_list": [...], # Additional context
|
||||
"ignore_self": true
|
||||
}
|
||||
```
|
||||
|
||||
**When to use:** Endpoints that fetch a single entity. Use the entity name as the field (jail, status, settings, etc.).
|
||||
|
||||
**Field naming:**
|
||||
- Primary entity uses its own name: `jail`, `status`, `settings`, etc.
|
||||
- Related or supplementary data uses descriptive names: `ignore_list`, `warnings`, `metadata`, etc.
|
||||
|
||||
#### Pattern 4: Command/Action Responses
|
||||
|
||||
Use `CommandResponse` for endpoints that execute commands:
|
||||
|
||||
```python
|
||||
from app.models.response import CommandResponse
|
||||
|
||||
class JailCommandResponse(CommandResponse):
|
||||
"""Generic response for jail control commands."""
|
||||
jail: str # Target identifier (optional)
|
||||
|
||||
# Returns:
|
||||
{
|
||||
"message": "Jail 'sshd' started.",
|
||||
"success": true,
|
||||
"jail": "sshd" # Optional: target identifier
|
||||
}
|
||||
```
|
||||
|
||||
**When to use:** POST/PUT/DELETE endpoints that perform operations (start jail, ban IP, update config, etc.).
|
||||
|
||||
**Fields:**
|
||||
- `message: str` — Human-readable result or error description.
|
||||
- `success: bool` — Operation succeeded (default: true). Use false for non-exception error handlers.
|
||||
- Optional domain-specific fields (jail, ip, etc.) to identify the affected resource.
|
||||
|
||||
#### Pattern 5: Aggregation Responses
|
||||
|
||||
Use domain-specific field names for aggregated data:
|
||||
|
||||
```python
|
||||
class BansByJailResponse(BaseModel):
|
||||
"""Response for ``GET /api/dashboard/bans/by-jail``."""
|
||||
jails: list[JailBanCount] # Aggregated per-jail data
|
||||
total: int # Total count across all jails
|
||||
|
||||
class BansByCountryResponse(BaseModel):
|
||||
"""Response for ``GET /api/dashboard/bans/by-country``."""
|
||||
countries: dict[str, int] # Country code → count
|
||||
country_names: dict[str, str] # Country code → name
|
||||
bans: list[DashboardBanItem] # Full list for rendering companion table
|
||||
total: int # Total ban count
|
||||
|
||||
# Returns:
|
||||
{
|
||||
"jails": [ { "jail": "sshd", "count": 42 }, ... ],
|
||||
"total": 500
|
||||
}
|
||||
```
|
||||
|
||||
**When to use:** Endpoints that return computed/aggregated data. Use field names that reflect the data (jails, countries, buckets, etc.).
|
||||
|
||||
### Summary Table
|
||||
|
||||
| Pattern | Used for | Field Names | Example |
|
||||
|---------|----------|---|---|
|
||||
| **PaginatedListResponse** | Paginated collections | `items`, `total`, `page`, `page_size` | `GET /api/dashboard/bans` |
|
||||
| **CollectionResponse** | Complete collections | `items`, `total` | `GET /api/config/jails` |
|
||||
| **Detail Response** | Single entity + metadata | Entity name + descriptors | `GET /api/jails/{name}` |
|
||||
| **CommandResponse** | Action results | `message`, `success` + optional identifiers | `POST /api/jails/{name}/start` |
|
||||
| **Aggregation Response** | Computed data | Domain-specific names | `GET /api/dashboard/bans/by-jail` |
|
||||
|
||||
### Rules
|
||||
|
||||
1. **Always wrap lists in `items` field** — Consistency aids frontend parsing.
|
||||
- ✅ `{ "items": [...], "total": 100 }`
|
||||
- ❌ `{ "jails": [...], "total": 100 }` (for list endpoints; OK for aggregations)
|
||||
|
||||
2. **Aggregation responses are exceptions** — They use domain-specific field names because the data represents computed results, not a simple list.
|
||||
- ✅ `{ "countries": {...}, "jails": [...], "total": 100 }`
|
||||
|
||||
3. **Every response with >1 item must include `total`** — Enables frontend to understand scale.
|
||||
|
||||
4. **Paginated responses must include `page` and `page_size`** — Enables the frontend to render pagination controls.
|
||||
|
||||
5. **No ad-hoc wrapper objects** — Don't invent new response shapes. Use the patterns above.
|
||||
|
||||
---
|
||||
|
||||
## 5. Pydantic Models
|
||||
|
||||
- Every model inherits from `pydantic.BaseModel`.
|
||||
|
||||
Reference in New Issue
Block a user