27 KiB
Tonehaus Project Briefing
Purpose
Tonehaus is a Symfony 7 music album review application extended for the Assignment 2 RESTful API brief. It combines a server-rendered website with a versioned JSON API, JWT authentication, external music metadata providers, and generated API documentation. The project is designed so the web UI and API share the same domain model and service layer rather than existing as separate systems.
Core Stack
- Symfony 7
- PHP with Doctrine ORM
- Twig templates
- SQLite by default, with Postgres support
- Guzzle for external API consumption
- LexikJWTAuthenticationBundle for JWT auth
- FOSRestBundle for API response handling
- NelmioApiDocBundle for OpenAPI documentation
- PHPUnit for functional and unit tests
Main Domain Model
User: authenticated account withROLE_USER,ROLE_MODERATOR, orROLE_ADMINAlbum: normalized album record, either external or user-createdAlbumTrack: persisted track list for each albumReview: user-authored review with title, content, rating, and timestampsSetting: key-value storage for site configuration and provider credentials
What The Application Does
Web Application
- lets users search for albums
- shows album pages with cover art, metadata, tracklists, review averages, and reviews
- allows logged-in users to create reviews from the album page
- allows users to create, edit, and delete their own user-created albums
- provides profile, password, and settings pages
- gives moderators/admins dashboards for moderation and site administration
- includes admin settings for external provider credentials and registration control
REST API
The API is versioned under /api/v1 and returns JSON.
Implemented endpoints:
GET /api/v1POST /api/v1/auth/loginGET /api/v1/albumsPOST /api/v1/albums/ingestGET /api/v1/albums/{id}GET /api/v1/albums/{id}/reviewsGET /api/v1/reviewsGET /api/v1/reviews/{id}POST /api/v1/reviewsPUT /api/v1/reviews/{id}PATCH /api/v1/reviews/{id}DELETE /api/v1/reviews/{id}
Automatic OpenAPI documentation is available at:
/api/docs/api/v1/docs/api/docs.json/api/v1/docs.json
API Endpoint Reference
This section documents every implemented API endpoint briefly, with purpose, authentication, key inputs, and the main response shape.
Example values below are representative of the current implementation and use realistic sample IDs such as tidal:251380836 and numeric review IDs.
GET /api/v1
- purpose: API index and discovery endpoint
- auth: none
- returns: API name, version, endpoint groups, top-level
links, and top-levelactions - HATEOAS: exposes
self,auth_login,albums,reviews,docs,docs_v1,schema, andschema_v1
Example request:
GET /api/v1
Accept: application/json
Example response:
{
"name": "Tonehaus API",
"version": "v1",
"endpoints": {
"auth": ["POST /api/v1/auth/login"],
"albums": [
"GET /api/v1/albums",
"POST /api/v1/albums/ingest (JWT)",
"GET /api/v1/albums/{id}",
"GET /api/v1/albums/{id}/reviews"
],
"reviews": [
"GET /api/v1/reviews",
"GET /api/v1/reviews/{id}",
"POST /api/v1/reviews (JWT)",
"PUT /api/v1/reviews/{id} (JWT)",
"PATCH /api/v1/reviews/{id} (JWT)",
"DELETE /api/v1/reviews/{id} (JWT)"
]
},
"links": {
"self": "/api/v1",
"auth_login": "/api/v1/auth/login",
"albums": "/api/v1/albums",
"reviews": "/api/v1/reviews",
"docs": "/api/docs"
},
"actions": {
"login": {
"method": "POST",
"href": "/api/v1/auth/login"
}
}
}
POST /api/v1/auth/login
- purpose: authenticate a user and issue a JWT for protected routes
- auth: none
- request body:
email,password - success response:
200with{ "token": "..." } - error response:
401for invalid credentials
Example request:
POST /api/v1/auth/login
Content-Type: application/json
Accept: application/json
{
"email": "user@example.com",
"password": "password123"
}
Example success response:
{
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9..."
}
Example error response:
{
"error": {
"code": "invalid_credentials",
"message": "Invalid credentials.",
"status": 401
}
}
GET /api/v1/albums
- purpose: search albums from the local catalog and the currently active external provider
- auth: none
- query parameters:
query,album,artist,genre,year_from,year_to,source,limit,mode - returns:
metawith source, limit, query, mode, and search profiledataas album resourcesstatsfor review aggregatessaved_idsfor already-ingested external albums- collection-level
linksandactions
- HATEOAS:
- collection links include
self,root, andingest - action metadata includes
ingest_album - each album resource is decorated with its own
linksandactions
- collection links include
Example request:
GET /api/v1/albums?query=4%3A44&artist=JAY-Z&limit=5&mode=fast
Accept: application/json
Example response:
{
"meta": {
"source": "all",
"limit": 5,
"query": "4:44",
"mode": "fast"
},
"data": [
{
"id": "tidal:251380836",
"name": "4:44",
"images": [
{ "url": "https://cdn.example.test/320.jpg" }
],
"artists": [
{ "name": "JAY-Z" }
],
"genres": ["Hip Hop"],
"release_date": "2017-06-30",
"total_tracks": 10,
"external_urls": {
"external": "https://tidal.com/browse/album/251380836"
},
"source": "external",
"links": {
"self": "/api/v1/albums/tidal%3A251380836",
"reviews": "/api/v1/albums/tidal%3A251380836/reviews",
"open_source": "https://tidal.com/browse/album/251380836"
},
"actions": {
"create_review": {
"method": "POST",
"href": "/api/v1/reviews",
"requires_auth": true,
"payload": {
"album_id": "tidal:251380836"
}
},
"ingest": {
"method": "POST",
"href": "/api/v1/albums/ingest",
"requires_auth": true,
"payload": {
"external_id": "tidal:251380836"
}
}
}
}
],
"stats": {
"tidal:251380836": {
"count": 2,
"avg": 8.5
}
},
"saved_ids": ["tidal:251380836"],
"links": {
"self": "/api/v1/albums?source=all&limit=5&mode=fast&query=4%3A44&artist=JAY-Z",
"root": "/api/v1",
"ingest": "/api/v1/albums/ingest"
},
"actions": {
"ingest_album": {
"method": "POST",
"href": "/api/v1/albums/ingest",
"requires_auth": true
}
}
}
POST /api/v1/albums/ingest
- purpose: persist an external album into the local database
- auth: JWT required
- request body:
- either
external_id - or
query - or
artistandalbum
- either
- success response:
201withdatafor the ingested album andtracks - main error responses:
400invalid JSON or missing lookup input401missing/invalid JWT404external album not found422album missing required metadata or tracks
Example request:
POST /api/v1/albums/ingest
Authorization: Bearer <jwt-token>
Content-Type: application/json
Accept: application/json
{
"external_id": "tidal:251380836"
}
Example response:
{
"data": {
"id": "tidal:251380836",
"name": "4:44",
"artists": [
{ "name": "JAY-Z" }
],
"genres": ["Hip Hop"],
"release_date": "2017-06-30",
"total_tracks": 10,
"source": "external",
"links": {
"self": "/api/v1/albums/tidal%3A251380836",
"reviews": "/api/v1/albums/tidal%3A251380836/reviews"
}
},
"tracks": [
{
"id": "track-1",
"disc": 1,
"track": 1,
"name": "Kill Jay Z",
"duration_ms": 205000
}
]
}
GET /api/v1/albums/{id}
- purpose: fetch one album by public identifier
- auth: none
- path parameter:
idstring public album ID such as a provider ID or user album ID - returns:
datafor one albumtracksliststatswith review count and average rating
- behaviour:
- if the album is not yet local but is available from the external provider, it can be hydrated and persisted automatically
- HATEOAS on
data:- links include
self,reviews, andopen_sourcewhen available - actions include
create_review - external albums also expose
ingest
- links include
Example request:
GET /api/v1/albums/tidal%3A251380836
Accept: application/json
Example response:
{
"data": {
"id": "tidal:251380836",
"name": "4:44",
"images": [
{ "url": "https://cdn.example.test/640.jpg" }
],
"artists": [
{ "name": "JAY-Z" }
],
"genres": ["Hip Hop"],
"release_date": "2017-06-30",
"total_tracks": 10,
"external_urls": {
"external": "https://tidal.com/browse/album/251380836"
},
"source": "external",
"links": {
"self": "/api/v1/albums/tidal%3A251380836",
"reviews": "/api/v1/albums/tidal%3A251380836/reviews",
"open_source": "https://tidal.com/browse/album/251380836"
},
"actions": {
"create_review": {
"method": "POST",
"href": "/api/v1/reviews",
"requires_auth": true,
"payload": {
"album_id": "tidal:251380836"
}
},
"ingest": {
"method": "POST",
"href": "/api/v1/albums/ingest",
"requires_auth": true,
"payload": {
"external_id": "tidal:251380836"
}
}
}
},
"tracks": [
{
"id": "track-1",
"disc": 1,
"track": 1,
"name": "Kill Jay Z",
"duration_ms": 205000
}
],
"stats": {
"count": 2,
"avg": 8.5
}
}
GET /api/v1/albums/{id}/reviews
- purpose: list reviews for a specific album
- auth: none
- path parameter:
idstring public album ID - returns:
dataas review resources scoped to that album- collection
links - collection
actions
- HATEOAS:
- collection links include
self,album, andreviews - collection actions include
create_review - each review item includes links back to its own item route and related album routes
- collection links include
Example request:
GET /api/v1/albums/tidal%3A251380836/reviews
Accept: application/json
Example response:
{
"data": [
{
"id": 12,
"title": "A dense and rewarding record",
"content": "This album becomes stronger on every listen and the production is excellent.",
"rating": 9,
"author": {
"id": 4,
"display_name": "Jake Andrews"
},
"created_at": "2026-04-02T11:40:21+00:00",
"updated_at": "2026-04-02T11:40:21+00:00",
"links": {
"self": "/api/v1/reviews/12",
"album": "/api/v1/albums/tidal%3A251380836",
"album_reviews": "/api/v1/albums/tidal%3A251380836/reviews"
}
}
],
"links": {
"self": "/api/v1/albums/tidal%3A251380836/reviews",
"album": "/api/v1/albums/tidal%3A251380836",
"reviews": "/api/v1/reviews"
},
"actions": {
"create_review": {
"method": "POST",
"href": "/api/v1/reviews",
"requires_auth": true
}
}
}
GET /api/v1/reviews
- purpose: list the latest reviews
- auth: none
- query parameters:
limitfrom1to100, default50 - returns:
dataas review resources- collection
links - collection
actions
- HATEOAS:
- collection links include
self,root, andcreate - collection actions include
create_review
- collection links include
Example request:
GET /api/v1/reviews?limit=10
Accept: application/json
Example response:
{
"data": [
{
"id": 12,
"title": "A dense and rewarding record",
"content": "This album becomes stronger on every listen and the production is excellent.",
"rating": 9,
"created_at": "2026-04-02T11:40:21+00:00",
"updated_at": "2026-04-02T11:40:21+00:00",
"author": {
"id": 4,
"display_name": "Jake Andrews"
},
"album": {
"id": "tidal:251380836",
"name": "4:44",
"source": "external"
},
"links": {
"self": "/api/v1/reviews/12",
"album": "/api/v1/albums/tidal%3A251380836",
"album_reviews": "/api/v1/albums/tidal%3A251380836/reviews"
}
}
],
"links": {
"self": "/api/v1/reviews?limit=10",
"root": "/api/v1",
"create": "/api/v1/reviews"
},
"actions": {
"create_review": {
"method": "POST",
"href": "/api/v1/reviews",
"requires_auth": true
}
}
}
GET /api/v1/reviews/{id}
- purpose: fetch one review by numeric ID
- auth: none
- path parameter:
idinteger review ID - returns: one review resource in
data - HATEOAS on the review resource:
- links include
self,album, andalbum_reviews - if the current user is allowed to change the review, action metadata can include
updateanddelete
- links include
Example request:
GET /api/v1/reviews/12
Accept: application/json
Example response:
{
"data": {
"id": 12,
"title": "A dense and rewarding record",
"content": "This album becomes stronger on every listen and the production is excellent.",
"rating": 9,
"created_at": "2026-04-02T11:40:21+00:00",
"updated_at": "2026-04-02T11:40:21+00:00",
"author": {
"id": 4,
"display_name": "Jake Andrews"
},
"album": {
"id": "tidal:251380836",
"name": "4:44",
"source": "external"
},
"links": {
"self": "/api/v1/reviews/12",
"album": "/api/v1/albums/tidal%3A251380836",
"album_reviews": "/api/v1/albums/tidal%3A251380836/reviews"
}
}
}
POST /api/v1/reviews
- purpose: create a new review for an existing album
- auth: JWT required
- request body:
album_idtitlecontentrating
- validation rules:
titlerequiredcontentrequired and minimum 20 charactersratingmust be between 1 and 10album_idmust resolve to a valid album with tracks
- success response:
201with the created review resource - main error responses:
400invalid JSON401authentication required404album not found422validation failure
Example request:
POST /api/v1/reviews
Authorization: Bearer <jwt-token>
Content-Type: application/json
Accept: application/json
{
"album_id": "tidal:251380836",
"title": "A dense and rewarding record",
"content": "This album becomes stronger on every listen and the production is excellent.",
"rating": 9
}
Example response:
{
"data": {
"id": 12,
"title": "A dense and rewarding record",
"content": "This album becomes stronger on every listen and the production is excellent.",
"rating": 9,
"created_at": "2026-04-02T11:40:21+00:00",
"updated_at": "2026-04-02T11:40:21+00:00",
"author": {
"id": 4,
"display_name": "Jake Andrews"
},
"album": {
"id": "tidal:251380836",
"name": "4:44",
"source": "external"
},
"links": {
"self": "/api/v1/reviews/12",
"album": "/api/v1/albums/tidal%3A251380836",
"album_reviews": "/api/v1/albums/tidal%3A251380836/reviews"
}
}
}
PUT /api/v1/reviews/{id}
- purpose: update an existing review
- auth: JWT required
- path parameter:
idinteger review ID - accepted body fields:
title,content,rating - authorization: review author, moderator, or admin
- success response:
200with updated review resource - main error responses:
400invalid JSON401missing/invalid JWT403forbidden422validation failure
- implementation note:
PUTcurrently shares the same update handler asPATCH
Example request:
PUT /api/v1/reviews/12
Authorization: Bearer <jwt-token>
Content-Type: application/json
Accept: application/json
{
"title": "Revisited after a week",
"content": "The writing is sharper than I first thought and the production has a lot more depth on repeat listens.",
"rating": 8
}
Example response:
{
"data": {
"id": 12,
"title": "Revisited after a week",
"content": "The writing is sharper than I first thought and the production has a lot more depth on repeat listens.",
"rating": 8,
"created_at": "2026-04-02T11:40:21+00:00",
"updated_at": "2026-04-02T12:05:44+00:00",
"author": {
"id": 4,
"display_name": "Jake Andrews"
},
"album": {
"id": "tidal:251380836",
"name": "4:44",
"source": "external"
},
"links": {
"self": "/api/v1/reviews/12",
"album": "/api/v1/albums/tidal%3A251380836",
"album_reviews": "/api/v1/albums/tidal%3A251380836/reviews"
}
}
}
PATCH /api/v1/reviews/{id}
- purpose: partially update an existing review
- auth: JWT required
- path parameter:
idinteger review ID - accepted body fields:
title,content,rating - authorization: review author, moderator, or admin
- success response:
200with updated review resource - main error responses:
400invalid JSON401missing/invalid JWT403forbidden422validation failure
Example request:
PATCH /api/v1/reviews/12
Authorization: Bearer <jwt-token>
Content-Type: application/json
Accept: application/json
{
"rating": 10
}
Example response:
{
"data": {
"id": 12,
"title": "A dense and rewarding record",
"content": "This album becomes stronger on every listen and the production is excellent.",
"rating": 10,
"created_at": "2026-04-02T11:40:21+00:00",
"updated_at": "2026-04-02T12:10:03+00:00",
"author": {
"id": 4,
"display_name": "Jake Andrews"
},
"album": {
"id": "tidal:251380836",
"name": "4:44",
"source": "external"
},
"links": {
"self": "/api/v1/reviews/12",
"album": "/api/v1/albums/tidal%3A251380836",
"album_reviews": "/api/v1/albums/tidal%3A251380836/reviews"
}
}
}
DELETE /api/v1/reviews/{id}
- purpose: delete a review
- auth: JWT required
- path parameter:
idinteger review ID - authorization: review author, moderator, or admin
- success response:
204 No Content - main error responses:
401missing/invalid JWT403forbidden404not found
Example request:
DELETE /api/v1/reviews/12
Authorization: Bearer <jwt-token>
Accept: application/json
Example response:
204 No Content
Architecture Summary
The project follows standard Symfony separation of concerns.
- controllers handle HTTP orchestration only
- services contain business logic and external API integration
- repositories handle domain-specific queries
- entities define persisted state
- forms handle server-side validation for web submissions
- security config and voters enforce route access and ownership rules
- event subscribers normalize API errors into JSON responses
This matters for the brief because the API is not implemented as one large controller. It is structured in a maintainable way using framework conventions and reusable services.
API Implementation Summary
The API implementation is centered around resource-based routes rather than action-style routes. Review endpoints expose collection and item resources, while album endpoints expose search, detail, ingestion, and album-specific review resources.
Key implementation points:
- uses descriptive REST-style URIs such as
/api/v1/reviewsand/api/v1/albums/{id}/reviews - uses correct HTTP verbs for retrieval, creation, update, and deletion
- returns JSON for both success and failure cases
- includes HATEOAS-style hypermedia in API responses through
linksandactions - uses JWT authentication for protected write operations
- excludes sensitive data such as user email and password hashes from API responses
- uses structured status codes including
200,201,204,401,403,404, and422 - provides generated OpenAPI documentation through Nelmio
The review API supports full CRUD behaviour for third-party clients. Public reads are available without authentication, while create, update, and delete require authenticated users. Authorization is more specific than simple login checks because review ownership is enforced with a ReviewVoter, allowing moderators and admins to manage any review while normal users can only manage their own.
The API also includes a useful sub-resource:
GET /api/v1/albums/{id}/reviews
This improves the resource model because reviews can be queried globally or in the context of a specific album.
The API also now includes a lightweight HATEOAS layer. The base API index exposes navigational links such as self, albums, reviews, and docs, plus action metadata for login. Album and review resources expose their own links and, where relevant, actions, for example:
- album resources link to
self,reviews, andopen_source - album resources expose actions such as
create_reviewandingest - review resources link back to their own item route and related album routes
- review collections expose a
create_reviewaction for discoverability
This improves the API from a client perspective because consumers do not need to hard-code every related route manually. Instead, the response itself advertises relevant next operations.
API Consumption Summary
The application consumes external music APIs through Guzzle and a provider abstraction rather than binding the UI directly to one upstream service.
Implemented external providers:
- Last.fm
- MusicBrainz
- TIDAL
Provider logic is normalized behind ExternalAlbumClientInterface, with concrete clients for each provider. This means the rest of the application receives one consistent album payload structure regardless of the source.
The consumed API data is used to:
- search external album catalogs
- populate album metadata
- fetch artists, genres, release dates, and track lists
- fetch cover art
- ingest external albums into the local database
- enrich album detail pages and search results in the UI
This is deep integration rather than a token demo call. External data directly affects the search experience, album display, save/ingest flow, and review context.
External Integration Details
Last.fm
Used for album search and metadata enrichment. Responses are cached and normalized into the project’s album format.
MusicBrainz
Used as an alternative provider and for structured release metadata. The project supports optional authenticated requests and cover-art URL generation via normalized output.
TIDAL
Used as another full external provider. The TidalClient implements OAuth 2.0 client credentials, caches access tokens, retries on expired auth, and converts JSON:API responses into the same normalized album contract used elsewhere.
This strengthens the project against the brief because it goes beyond a single trivial API call and includes a second non-trivial external integration path.
Security Summary
Security is handled at multiple layers:
- JWT authentication for protected API routes
- Symfony access control rules for route-level protection
- role hierarchy: admin > moderator > user
ReviewVoterfor ownership-based edit/delete permissions- normalized JSON authentication failures for API clients
- exclusion of sensitive user fields from serialized API responses
This is stronger than basic coursework authentication because it combines framework security with resource-level authorization.
Error Handling and Robustness
API error handling is centralized so clients receive JSON errors rather than HTML exception pages.
The project includes:
- a shared API response helper
- exception subscribers that normalize failures
- JWT failure subscribers for missing, invalid, or expired tokens
- validation logic for malformed JSON and invalid field values
- server-side handling for missing albums, forbidden edits, and unsupported methods
This improves client usability and supports a more production-like API design.
Testing Summary
The project includes both functional and unit tests.
Functional API tests cover:
- missing JWT
- invalid JWT
- malformed JSON
- validation failures
- forbidden review updates
- missing album resources
- protection of sensitive author data
- hypermedia links and action metadata on API index, album, and album-review responses
Unit tests cover provider-specific service behaviour, including TIDAL payload normalization and provider routing.
This supports the brief’s emphasis on working, demonstrable features rather than partially implemented code.
How The Project Meets The Brief
API Implementation
The brief requires a usable RESTful API capable of listing, reading, creating, editing, and deleting album reviews in JSON. This project meets that with:
GET,POST,PUT,PATCH, andDELETEreview endpoints- JWT-protected write routes
- appropriate resource names
- JSON request and response formats
- HATEOAS-style
linksandactionsto improve discoverability - status-code-based error handling
- sub-resource support through album review listing
- automatically generated OpenAPI documentation
API Consumption
The brief requires at least two significantly different examples of API interaction and integration into the review website. This project meets that with:
- external album search
- album metadata retrieval
- track list retrieval
- cover art integration
- album ingestion into local storage
- multiple provider implementations
The consumed data is visible in the website itself, especially on search results and album detail pages.
Documentation and Report Potential
The project already has strong source material for the report because it includes:
- generated OpenAPI docs
- explicit controller routes and OpenAPI attributes
- centralized security configuration
- reusable external service classes
- tests that demonstrate expected API behaviour
Honest Technical Caveats
PUTandPATCHcurrently share the same update action, soPUTbehaves more like a flexible update than a strict full replacement- local verification in this shell was limited because
phpis not installed here, so PHPUnit was not run live during this session - some observability headers in album search are still named with
Lastfmeven though the provider layer now supports multiple providers
These are not major blockers, but they are worth understanding if you want the final report to sound precise and credible.
Strongest Points To Emphasize In A Final Report
- the API is versioned and resource-oriented
- the API now includes lightweight HATEOAS support through response
linksandactions - write operations are protected with JWT rather than session auth
- review ownership is enforced through a voter, not just a login check
- external APIs are abstracted behind a provider interface
- external data is deeply integrated into the UI and persisted catalog
- OpenAPI documentation is generated automatically
- the project includes automated tests for important API behaviour
Short Project Summary
Tonehaus is a Symfony-based music review platform extended into a RESTful web application. It implements a versioned JSON API for review CRUD and album access, secures write routes with JWT, consumes multiple external music providers through Guzzle and a normalized service layer, and integrates that data directly into album discovery, metadata display, tracklists, and review workflows. The architecture is modular, test-backed, and aligned with the main goals of the Assignment 2 brief.