ML-152: add /api/v1/ version prefix to all API routes

This commit is contained in:
Claudio Ortolina
2026-04-30 11:57:43 +01:00
parent 70d5f3bf61
commit c489fde1cc
6 changed files with 77 additions and 28 deletions
@@ -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 -->
+3 -3
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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