ML-152: add /api/v1/ version prefix to all API routes
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
---
|
||||
id: ML-152
|
||||
title: Add API version prefix to collection endpoints
|
||||
status: To Do
|
||||
status: Done
|
||||
assignee: []
|
||||
created_date: '2026-04-30 10:48'
|
||||
updated_date: '2026-04-30 10:57'
|
||||
labels:
|
||||
- api
|
||||
- versioning
|
||||
@@ -37,9 +38,57 @@ Update:
|
||||
|
||||
## Acceptance Criteria
|
||||
<!-- AC:BEGIN -->
|
||||
- [ ] #1 All collection API routes live under `/api/v1/collection/*`
|
||||
- [ ] #2 Existing `/api/collection/*` routes are removed or redirect to v1 with 301
|
||||
- [ ] #3 Controller and JSON tests pass against the new `/api/v1/` paths
|
||||
- [ ] #4 `test/prod.hurl` references updated to new paths
|
||||
- [ ] #5 `docs/architecture.md` updated with new route paths
|
||||
- [x] #1 All collection API routes live under `/api/v1/collection/*`
|
||||
- [x] #2 Existing `/api/collection/*` routes are removed or redirect to v1 with 301
|
||||
- [x] #3 Controller and JSON tests pass against the new `/api/v1/` paths
|
||||
- [x] #4 `test/prod.hurl` references updated to new paths
|
||||
- [x] #5 `docs/architecture.md` updated with new route paths
|
||||
<!-- AC:END -->
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
<!-- SECTION:PLAN:BEGIN -->
|
||||
## Implementation Plan
|
||||
|
||||
**Scope**: Version ALL routes under `/api` to `/api/v1/`, not just collection endpoints. Affected routes:
|
||||
- `/api/v1/collection`
|
||||
- `/api/v1/collection/latest`
|
||||
- `/api/v1/collection/random`
|
||||
- `/api/v1/collection/on_this_day`
|
||||
- `/api/v1/assets/:transform_payload`
|
||||
- `/api/v1/backup`
|
||||
|
||||
**Decision**: Remove old routes (no 301 redirects) — user controls all consumers.
|
||||
|
||||
**Files to change**:
|
||||
1. `lib/music_library_web/router.ex` — Move all routes from `/api` to `/api/v1`
|
||||
2. `test/music_library_web/controllers/collection_controller_test.exs` — Update paths to `/api/v1/collection/*`
|
||||
3. `test/prod.hurl` — Update `/api/collection/latest` → `/api/v1/collection/latest`
|
||||
4. `docs/architecture.md` — Update route table entries for CollectionController, AssetController (API route), ArchiveController (API route)
|
||||
5. Asset controller tests (if any) — Check for `/api/assets/` references
|
||||
<!-- SECTION:PLAN:END -->
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
<!-- SECTION:NOTES:BEGIN -->
|
||||
## Implementation complete
|
||||
|
||||
### Files changed
|
||||
|
||||
1. **`lib/music_library_web/router.ex`** — Changed `scope "/api"` to `scope "/api/v1"` for all 6 API routes (collection × 4 + assets + backup). Old routes removed per user decision.
|
||||
|
||||
2. **`lib/music_library_web/controllers/collection_json.ex`** — Updated `cover_url` and `thumb_url` path sigils from `~p"/api/assets/..."` to `~p"/api/v1/assets/..."`.
|
||||
|
||||
3. **`test/music_library_web/controllers/collection_controller_test.exs`** — Updated all 4 route paths, 4 describe strings, and 2 expected JSON URLs to `/api/v1/...`.
|
||||
|
||||
4. **`test/prod.hurl`** — Updated both API references from `/api/collection/latest` to `/api/v1/collection/latest`.
|
||||
|
||||
5. **`docs/architecture.md`** — Updated ArchiveController, AssetController, and CollectionController route entries to `/api/v1/...`.
|
||||
|
||||
### Test results
|
||||
```
|
||||
CollectionControllerTest: 5 passed
|
||||
AssetControllerTest: 9 passed
|
||||
ArchiveControllerTest: 1 passed
|
||||
```
|
||||
<!-- SECTION:NOTES:END -->
|
||||
|
||||
@@ -334,9 +334,9 @@ All authenticated routes live inside a single `live_session` with three `on_moun
|
||||
| `SessionController` | `/login`, `/sessions/create` | Login/logout |
|
||||
| `HealthController` | `/health` | Health check |
|
||||
| `LastFmController` | `/auth/last_fm/callback` | Last.fm OAuth |
|
||||
| `ArchiveController` | `/backup`, `/api/backup` | Database backup download (API route requires token) |
|
||||
| `AssetController` | `/assets/:transform_payload`, `/public/assets/:transform_payload`, `/api/assets/:transform_payload` | Serve images with transforms (public route for emails, API route requires token) |
|
||||
| `CollectionController` | `/api/collection/*` | JSON API for collection queries |
|
||||
| `ArchiveController` | `/backup`, `/api/v1/backup` | Database backup download (API route requires token) |
|
||||
| `AssetController` | `/assets/:transform_payload`, `/public/assets/:transform_payload`, `/api/v1/assets/:transform_payload` | Serve images with transforms (public route for emails, API route requires token) |
|
||||
| `CollectionController` | `/api/v1/collection/*` | JSON API for collection queries |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -33,8 +33,8 @@ defmodule MusicLibraryWeb.CollectionJSON do
|
||||
purchased_at: record.purchased_at,
|
||||
artists: Enum.map(record.artists, & &1.name),
|
||||
title: record.title,
|
||||
cover_url: url(~p"/api/assets/#{Transform.new(hash: record.cover_hash)}"),
|
||||
thumb_url: url(~p"/api/assets/#{Transform.new(hash: record.cover_hash, width: 480)}")
|
||||
cover_url: url(~p"/api/v1/assets/#{Transform.new(hash: record.cover_hash)}"),
|
||||
thumb_url: url(~p"/api/v1/assets/#{Transform.new(hash: record.cover_hash, width: 480)}")
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -123,7 +123,7 @@ defmodule MusicLibraryWeb.Router do
|
||||
end
|
||||
end
|
||||
|
||||
scope "/api", MusicLibraryWeb do
|
||||
scope "/api/v1", MusicLibraryWeb do
|
||||
pipe_through :api
|
||||
|
||||
get "/collection/latest", CollectionController, :latest
|
||||
|
||||
@@ -15,10 +15,10 @@ defmodule MusicLibraryWeb.CollectionControllerTest do
|
||||
describe "authentication" do
|
||||
test "all API endpoints require a bearer token", %{conn: conn} do
|
||||
for path <- [
|
||||
~p"/api/collection/latest",
|
||||
~p"/api/collection/random",
|
||||
~p"/api/collection",
|
||||
~p"/api/collection/on_this_day"
|
||||
~p"/api/v1/collection/latest",
|
||||
~p"/api/v1/collection/random",
|
||||
~p"/api/v1/collection",
|
||||
~p"/api/v1/collection/on_this_day"
|
||||
] do
|
||||
assert get(conn, path).status == 401,
|
||||
"expected 401 for unauthenticated GET #{path}"
|
||||
@@ -26,20 +26,20 @@ defmodule MusicLibraryWeb.CollectionControllerTest do
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /api/collection/latest" do
|
||||
describe "GET /api/v1/collection/latest" do
|
||||
setup [:create_record]
|
||||
|
||||
test "returns the latest record", %{conn: conn, record: record} do
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header("authorization", "Bearer #{api_token()}")
|
||||
|> get(~p"/api/collection/latest")
|
||||
|> get(~p"/api/v1/collection/latest")
|
||||
|
||||
assert json_response(conn, 200) == expected_record_json(record)
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /api/collection/random" do
|
||||
describe "GET /api/v1/collection/random" do
|
||||
setup [:create_record]
|
||||
|
||||
# We're not testing random here - the query is solid enough
|
||||
@@ -47,20 +47,20 @@ defmodule MusicLibraryWeb.CollectionControllerTest do
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header("authorization", "Bearer #{api_token()}")
|
||||
|> get(~p"/api/collection/random")
|
||||
|> get(~p"/api/v1/collection/random")
|
||||
|
||||
assert json_response(conn, 200) == expected_record_json(record)
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /api/collection" do
|
||||
describe "GET /api/v1/collection" do
|
||||
setup [:create_record]
|
||||
|
||||
test "returns a paginated list of records", %{conn: conn, record: record} do
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header("authorization", "Bearer #{api_token()}")
|
||||
|> get(~p"/api/collection")
|
||||
|> get(~p"/api/v1/collection")
|
||||
|
||||
assert json_response(conn, 200) == %{
|
||||
"total" => 1,
|
||||
@@ -71,14 +71,14 @@ defmodule MusicLibraryWeb.CollectionControllerTest do
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /api/collection/on_this_day" do
|
||||
describe "GET /api/v1/collection/on_this_day" do
|
||||
setup [:create_record]
|
||||
|
||||
test "returns a list of records", %{conn: conn, record: record} do
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header("authorization", "Bearer #{api_token()}")
|
||||
|> get(~p"/api/collection/on_this_day?date=2025-06-21")
|
||||
|> get(~p"/api/v1/collection/on_this_day?date=2025-06-21")
|
||||
|
||||
assert json_response(conn, 200) == %{
|
||||
"records" => [expected_record_json(record)]
|
||||
@@ -98,9 +98,9 @@ defmodule MusicLibraryWeb.CollectionControllerTest do
|
||||
"artists" => Enum.map(record.artists, & &1.name),
|
||||
"title" => record.title,
|
||||
"cover_url" =>
|
||||
"http://localhost:4002/api/assets/eyJoYXNoIjoiNTk5NDA3RERGNjk5MDdENEE2MEZFMTNDQ0FBODI0RDI1Q0YwOERDMTI0RkQ2QUEzRThFN0VDRDk4Qzg4NUZGRSIsIndpZHRoIjpudWxsfQ",
|
||||
"http://localhost:4002/api/v1/assets/eyJoYXNoIjoiNTk5NDA3RERGNjk5MDdENEE2MEZFMTNDQ0FBODI0RDI1Q0YwOERDMTI0RkQ2QUEzRThFN0VDRDk4Qzg4NUZGRSIsIndpZHRoIjpudWxsfQ",
|
||||
"thumb_url" =>
|
||||
"http://localhost:4002/api/assets/eyJoYXNoIjoiNTk5NDA3RERGNjk5MDdENEE2MEZFMTNDQ0FBODI0RDI1Q0YwOERDMTI0RkQ2QUEzRThFN0VDRDk4Qzg4NUZGRSIsIndpZHRoIjo0ODB9"
|
||||
"http://localhost:4002/api/v1/assets/eyJoYXNoIjoiNTk5NDA3RERGNjk5MDdENEE2MEZFMTNDQ0FBODI0RDI1Q0YwOERDMTI0RkQ2QUEzRThFN0VDRDk4Qzg4NUZGRSIsIndpZHRoIjo0ODB9"
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
+2
-2
@@ -7,11 +7,11 @@ Location: https://music-library.claudio-ortolina.org/
|
||||
# API requires a valid token
|
||||
|
||||
# GET latest record
|
||||
GET https://music-library.claudio-ortolina.org/api/collection/latest
|
||||
GET https://music-library.claudio-ortolina.org/api/v1/collection/latest
|
||||
Content-Type: application/json
|
||||
HTTP 401
|
||||
|
||||
GET https://music-library.claudio-ortolina.org/api/collection/latest
|
||||
GET https://music-library.claudio-ortolina.org/api/v1/collection/latest
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {{api_token}}
|
||||
HTTP 200
|
||||
|
||||
Reference in New Issue
Block a user