From c489fde1ccfbfc9028054ce98407ee3993f2960a Mon Sep 17 00:00:00 2001 From: Claudio Ortolina Date: Thu, 30 Apr 2026 11:57:43 +0100 Subject: [PATCH] ML-152: add /api/v1/ version prefix to all API routes --- ...-version-prefix-to-collection-endpoints.md | 61 +++++++++++++++++-- docs/architecture.md | 6 +- .../controllers/collection_json.ex | 4 +- lib/music_library_web/router.ex | 2 +- .../collection_controller_test.exs | 28 ++++----- test/prod.hurl | 4 +- 6 files changed, 77 insertions(+), 28 deletions(-) diff --git a/backlog/tasks/ml-152 - Add-API-version-prefix-to-collection-endpoints.md b/backlog/tasks/ml-152 - Add-API-version-prefix-to-collection-endpoints.md index 340876cf..e812b347 100644 --- a/backlog/tasks/ml-152 - Add-API-version-prefix-to-collection-endpoints.md +++ b/backlog/tasks/ml-152 - Add-API-version-prefix-to-collection-endpoints.md @@ -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 -- [ ] #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 + +## Implementation Plan + + +## 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 + + +## Implementation Notes + + +## 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 +``` + diff --git a/docs/architecture.md b/docs/architecture.md index f28d69f6..0d3099bf 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -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 | --- diff --git a/lib/music_library_web/controllers/collection_json.ex b/lib/music_library_web/controllers/collection_json.ex index 5823f6a8..0b9d9f15 100644 --- a/lib/music_library_web/controllers/collection_json.ex +++ b/lib/music_library_web/controllers/collection_json.ex @@ -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 diff --git a/lib/music_library_web/router.ex b/lib/music_library_web/router.ex index 21550deb..c60ca0f7 100644 --- a/lib/music_library_web/router.ex +++ b/lib/music_library_web/router.ex @@ -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 diff --git a/test/music_library_web/controllers/collection_controller_test.exs b/test/music_library_web/controllers/collection_controller_test.exs index e07fe376..c88afeee 100644 --- a/test/music_library_web/controllers/collection_controller_test.exs +++ b/test/music_library_web/controllers/collection_controller_test.exs @@ -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 diff --git a/test/prod.hurl b/test/prod.hurl index 46581828..2bb2b975 100644 --- a/test/prod.hurl +++ b/test/prod.hurl @@ -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