feat(backend): add deprecation middleware and API versioning support
- Add deprecation middleware for warning headers on sunset endpoints - Add jails_v2 router for API v2 migration path - Update CI workflow with new test coverage - Update API versioning documentation - Remove completed tasks from Tasks.md
This commit is contained in:
@@ -23,8 +23,8 @@ This document explains when and how to version endpoints, how deprecation works,
|
||||
/api/v{major}/<resource>/<path>
|
||||
```
|
||||
|
||||
- **v1** — current version (2026-05-02)
|
||||
- **v2** — reserved for future breaking changes
|
||||
- **v1** — current version (released 2026-05-02)
|
||||
- **v2** — reserved; skeleton router deployed at `/api/v2/jails` but **not yet active** for production traffic
|
||||
- **PATCH** versions (v1.1, v1.2) are **not** used; only **major** version bumps indicate breaking changes
|
||||
- The OpenAPI schema is always available at `/api/openapi.json` regardless of version
|
||||
|
||||
@@ -56,14 +56,35 @@ These do **not** require a version bump.
|
||||
When an endpoint is deprecated:
|
||||
|
||||
1. The endpoint **remains functional** for a minimum of **6 months** from the `Sunset` date
|
||||
2. Response headers are added:
|
||||
2. Response headers are added to every 2xx response:
|
||||
```
|
||||
Deprecation: true
|
||||
Sunset: <RFC-5322 date>
|
||||
Link: <https://bangui.example.com/api/v2/...>; rel="successor-version"
|
||||
```
|
||||
3. The OpenAPI schema marks the endpoint with `deprecated: true`
|
||||
4. Documentation is updated to show the endpoint as deprecated
|
||||
3. The endpoint is registered in the deprecation middleware (``app/middleware/deprecation.py``)
|
||||
4. The OpenAPI schema marks the endpoint with `deprecated: true`
|
||||
5. Documentation is updated to show the endpoint as deprecated
|
||||
|
||||
### Implementing Deprecation Headers
|
||||
|
||||
The ``DeprecationHeaderMiddleware`` (``app/middleware/deprecation.py``) automatically injects
|
||||
the correct headers for any registered deprecated endpoint. To schedule an endpoint for removal:
|
||||
|
||||
```python
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from app.middleware.deprecation import register_deprecated_endpoint
|
||||
|
||||
# Example: deprecate /api/v1/jails on 2026-11-03 (6 months from v2 release)
|
||||
register_deprecated_endpoint(
|
||||
path_prefix="/api/v1/jails",
|
||||
sunset_date=datetime(2026, 11, 3, tzinfo=timezone.utc),
|
||||
successor_url="/api/v2/jails",
|
||||
)
|
||||
```
|
||||
|
||||
The middleware runs on every response; if the request path matches a registered deprecated prefix,
|
||||
the appropriate headers are appended before the response is returned.
|
||||
|
||||
---
|
||||
|
||||
@@ -81,19 +102,21 @@ router = APIRouter(prefix="/api/v1/my-resource", tags=["My Resource"])
|
||||
|
||||
1. Create a new router file (e.g., `routers/my_resource_v2.py`) with the v2 prefix:
|
||||
```python
|
||||
router = APIRouter(prefix="/api/v2/my-resource", tags=["My Resource"])
|
||||
router = APIRouter(prefix="/api/v2/my-resource", tags=["My Resource (v2)"])
|
||||
```
|
||||
2. Copy or adapt the v1 handler logic as needed
|
||||
2. Copy or adapt the v1 handler logic as needed. Extract shared business logic into
|
||||
a **service layer function** so both routers call the same underlying code.
|
||||
3. Register the new router in `app/main.py`:
|
||||
```python
|
||||
app.include_router(my_resource_v2.router)
|
||||
```
|
||||
4. Add deprecation headers to the **old** v1 router by marking it deprecated in the OpenAPI spec
|
||||
4. Register the v1 endpoint for deprecation headers (see §4 above)
|
||||
5. Update this document to reflect the new version lifecycle
|
||||
|
||||
### Keeping routers DRY
|
||||
|
||||
If v1 and v2 share logic, extract business logic into a **service layer function** and call it from both router handlers. Routers should only contain HTTP concerns (parameters, responses, status codes).
|
||||
Routers should only contain HTTP concerns (parameters, responses, status codes). Business logic
|
||||
belongs in the service layer. Both v1 and v2 handlers can call the same service function.
|
||||
|
||||
---
|
||||
|
||||
@@ -107,6 +130,8 @@ const BASE_URL: string = import.meta.env.VITE_API_URL ?? "/api/v1";
|
||||
|
||||
All endpoint paths in `frontend/src/api/endpoints.ts` are defined as relative paths (e.g., `/bans`, `/jails`) and are appended to `BASE_URL` at runtime.
|
||||
|
||||
When v2 is released, update ``VITE_API_URL`` in the environment configuration to point to `/api/v2`.
|
||||
|
||||
---
|
||||
|
||||
## 7. OpenAPI / Documentation
|
||||
@@ -118,8 +143,24 @@ All endpoint paths in `frontend/src/api/endpoints.ts` are defined as relative pa
|
||||
|
||||
---
|
||||
|
||||
## 8. Version History
|
||||
## 8. CI Breaking-Change Checks
|
||||
|
||||
A GitHub Actions job runs on every pull request to detect breaking OpenAPI changes:
|
||||
|
||||
- ``openapi-breaking-changes`` job (PR only): generates the current OpenAPI spec and
|
||||
compares it against the baseline committed on the last push to `main`. If any breaking
|
||||
changes are found, the job fails and the PR cannot be merged.
|
||||
- ``openapi-baseline-commit`` job (main push only): generates and commits the current
|
||||
OpenAPI spec as the new baseline for future PR comparisons.
|
||||
|
||||
To trigger the baseline update, push to main after merging a version bump or any change
|
||||
that legitimately alters the OpenAPI surface.
|
||||
|
||||
---
|
||||
|
||||
## 9. Version History
|
||||
|
||||
| Version | Status | Released | Sunset Date | Notes |
|
||||
|---------|--------|---------|-------------|-------|
|
||||
| v1 | **Current** | 2026-05-02 | — | Initial versioning; all endpoints moved from `/api/` to `/api/v1/` |
|
||||
| v1 | **Current** | 2026-05-02 | — | Initial versioning; all endpoints moved from `/api/` to `/api/v1/` |
|
||||
| v2 | **Reserved — skeleton active, endpoints not yet available** | — | — | Router skeleton at `app/routers/jails_v2.py`; real endpoints will be added before activation |
|
||||
@@ -1,30 +1,3 @@
|
||||
### Issue #55: MEDIUM - Correlation ID Scope Is Module-Level (Resets on HMR)
|
||||
|
||||
**Where found**:
|
||||
- `frontend/src/api/client.ts:26-39` – `sessionCorrelationId` stored as a module variable
|
||||
|
||||
**Why this is needed**:
|
||||
Hot Module Replacement (HMR) re-evaluates modules, generating a new correlation ID mid-session. Distributed traces lose continuity, making it impossible to correlate logs from the same user session.
|
||||
|
||||
**Goal**:
|
||||
Persist the correlation ID across module re-evaluations for the lifetime of the browser tab.
|
||||
|
||||
**What to do**:
|
||||
1. Store the correlation ID in `sessionStorage` on first generation; read from there on subsequent module evaluations.
|
||||
2. Clear it on logout.
|
||||
|
||||
**Possible traps and issues**:
|
||||
- `sessionStorage` is tab-local, which is the desired scope.
|
||||
- Ensure the ID is not leaked in URLs or logs.
|
||||
|
||||
**Docs changes needed**:
|
||||
- Add comment explaining the persistence strategy in `client.ts`.
|
||||
|
||||
**Doc references**:
|
||||
- `frontend/src/api/client.ts` – `getSessionCorrelationId()`
|
||||
|
||||
---
|
||||
|
||||
### Issue #56: MEDIUM - No API Versioning or Deprecation Strategy
|
||||
|
||||
**Where found**:
|
||||
|
||||
Reference in New Issue
Block a user