18 KiB
Assignment 2 Report: Tonehaus REST API
Part 1: API Documentation
1. API Overview
Tonehaus extends the album review website with a versioned JSON API under /api/v1. The API supports two main concerns:
- discovery and ingestion of album data
- creation and management of user reviews
The implementation is resource-oriented. Public read endpoints are available without authentication, while write operations require JWT authentication. The API is documented manually in this report and automatically through Nelmio OpenAPI documentation at:
/api/docs/api/v1/docs/api/docs.json/api/v1/docs.json
2. Authentication
Protected endpoints use bearer-token authentication with JSON Web Tokens (JWT). Clients obtain a token by sending credentials to:
POST /api/v1/auth/login
The token must then be included on protected requests:
Authorization: Bearer <jwt-token>
Protected routes:
POST /api/v1/albums/ingestPOST /api/v1/reviewsPUT /api/v1/reviews/{id}PATCH /api/v1/reviews/{id}DELETE /api/v1/reviews/{id}
3. Common Response Format
Success
Success payloads are JSON objects. Most collection/detail routes return a top-level data key. Some album endpoints also return meta, tracks, stats, or saved_ids.
Error
API failures are normalized into one JSON error envelope:
{
"error": {
"code": "validation_error",
"message": "Review payload failed validation.",
"status": 422,
"details": {
"fields": {
"content": "Content must be at least 20 characters."
}
}
}
}
Common error codes include:
authentication_requiredinvalid_credentialsinvalid_tokentoken_expiredvalidation_erroralbum_not_foundexternal_album_not_foundforbiddennot_foundmethod_not_allowedinternal_server_error
4. Resource Models
Review Resource
{
"id": 12,
"title": "A dense and rewarding record",
"content": "This album becomes stronger on every listen...",
"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"
}
}
Album Resource
{
"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"
}
Track Resource
{
"id": "track-1",
"disc_number": 1,
"track_number": 1,
"name": "Kill Jay Z",
"duration_ms": 205000,
"preview_url": null
}
Album identifiers are public string IDs rather than numeric database IDs. Depending on provider and source, examples include:
tidal:251380836mbid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxu_ab12cd34ef56for user-created albums
5. Endpoint Reference
GET /api/v1
Purpose: API index and discovery endpoint.
Auth: None
Response:
200 OK
Example:
{
"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)"
]
}
}
POST /api/v1/auth/login
Purpose: authenticate a user and issue a JWT.
Auth: None
Request body:
{
"email": "user@example.com",
"password": "password123"
}
Responses:
200 OKtoken issued401 Unauthorizedinvalid credentials
Success example:
{
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9..."
}
GET /api/v1/albums
Purpose: search albums from the local catalog and the currently configured external provider.
Auth: None
Query parameters:
queryfree-text searchalbumalbum title filterartistartist filtergenregenre filteryear_frominclusive lower year boundyear_toinclusive upper year boundsourceall,external, oruserlimitresult limit, capped server-sidemodefullorfast
Example:
GET /api/v1/albums?query=4%3A44&artist=JAY-Z&limit=5&mode=fast
Accept: application/json
Responses:
200 OK
Success example:
{
"meta": {
"source": "all",
"limit": 5,
"query": "4:44",
"mode": "fast",
"profile": {
"external_provider": "tidal"
}
},
"data": [
{
"id": "tidal:251380836",
"name": "4:44",
"artists": [{ "name": "JAY-Z" }],
"genres": ["Hip Hop"],
"release_date": "2017-06-30",
"total_tracks": 10,
"images": [{ "url": "https://cdn.example.test/320.jpg" }],
"external_urls": {
"external": "https://tidal.com/browse/album/251380836"
},
"source": "external"
}
],
"stats": {
"tidal:251380836": {
"count": 2,
"avg": 8.5
}
},
"saved_ids": ["tidal:251380836"]
}
POST /api/v1/albums/ingest
Purpose: persist an external album into local storage.
Auth: JWT required
Request body rules:
- either
external_idmust be supplied - or
query - or a combination of
artistandalbum
Example:
{
"external_id": "tidal:251380836"
}
Responses:
201 Created400 Bad Requestinvalid JSON or missing identifier/query401 Unauthorized404 Not Foundexternal album not found422 Unprocessable Entityalbum missing metadata or tracks
Success response includes:
dataalbum payloadtracksnormalized track list
GET /api/v1/albums/{id}
Purpose: return one album by public ID.
Auth: None
Path parameter:
idpublic album identifier string
Responses:
200 OK404 Not Found422 Unprocessable Entityalbum payload exists upstream but is not ingestable
Success example:
{
"data": {
"id": "tidal:251380836",
"name": "4:44",
"artists": [{ "name": "JAY-Z" }],
"genres": ["Hip Hop"],
"release_date": "2017-06-30",
"total_tracks": 10,
"images": [{ "url": "https://cdn.example.test/640.jpg" }],
"external_urls": {
"external": "https://tidal.com/browse/album/251380836"
},
"source": "external"
},
"tracks": [
{
"id": "track-1",
"disc_number": 1,
"track_number": 1,
"name": "Kill Jay Z",
"duration_ms": 205000,
"preview_url": null
}
],
"stats": {
"count": 2,
"avg": 8.5
}
}
GET /api/v1/albums/{id}/reviews
Purpose: list reviews for one album.
Auth: None
Responses:
200 OK404 Not Found
Example response:
{
"data": [
{
"id": 12,
"title": "A dense and rewarding record",
"content": "This album becomes stronger on every listen...",
"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"
}
]
}
GET /api/v1/reviews
Purpose: list recent reviews.
Auth: None
Query parameters:
limitinteger, clamped to1-100, default50
Responses:
200 OK
Example:
GET /api/v1/reviews?limit=10
Accept: application/json
GET /api/v1/reviews/{id}
Purpose: fetch one review by numeric ID.
Auth: None
Responses:
200 OK404 Not Found
POST /api/v1/reviews
Purpose: create a review for an existing album.
Auth: JWT required
Request body:
{
"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
}
Validation rules:
album_idmust resolve to an existing album with a track listtitleis requiredcontentis required and must be at least 20 charactersratingmust be between 1 and 10
Responses:
201 Created400 Bad Requestinvalid JSON401 Unauthorized404 Not Foundalbum not found422 Unprocessable Entityvalidation failure
PUT /api/v1/reviews/{id}
Purpose: update an existing review.
Auth: JWT required
Current implementation note: PUT and PATCH are handled by the same update action. In practice, the API currently accepts partial payloads on both methods rather than enforcing full replacement semantics for PUT.
Accepted fields:
titlecontentrating
Validation:
- at least one updatable field is required
titlecannot be emptycontentcannot be empty and must be at least 20 charactersratingmust be between 1 and 10- only the review author, moderator, or admin may modify the review
Responses:
200 OK400 Bad Request401 Unauthorized403 Forbidden404 Not Found422 Unprocessable Entity
Example:
{
"title": "Revisited after a few weeks",
"content": "The sequencing is much better than I first thought, and the writing is sharper too.",
"rating": 8
}
PATCH /api/v1/reviews/{id}
Purpose: partially update an existing review.
Auth: JWT required
Example:
{
"rating": 10
}
Responses:
200 OK400 Bad Request401 Unauthorized403 Forbidden404 Not Found422 Unprocessable Entity
DELETE /api/v1/reviews/{id}
Purpose: delete a review.
Auth: JWT required
Authorization:
- author of the review
- moderator
- admin
Responses:
204 No Content401 Unauthorized403 Forbidden404 Not Found
6. Automatic Documentation and Testing Support
The API is additionally exposed through generated OpenAPI documentation with Nelmio. This gives the project a professional, interactive documentation layer on top of the written report. The application also includes automated tests for:
- JWT protection
- normalized JSON error envelopes
- validation failures
- forbidden review updates
- protection of sensitive user data in API responses
- provider-specific payload normalization, including TIDAL
Part 2: Code Report
Introduction
The Tonehaus application was originally a server-rendered album review website, but for Assignment 2 it has been extended into a REST-oriented application with both API implementation and API consumption. The final system does not simply expose review data as JSON. It also uses external music services to build and enrich the album catalog that reviews depend on. As a result, the API and the web UI now share the same underlying album model, review model, and security rules.
API Implementation
The implemented API is versioned under /api/v1, which is preferable to exposing unversioned endpoints because it leaves room for future iterations without breaking existing clients. The main resources are reviews and albums. Review routes cover list, read, create, update, and delete operations. Album routes cover search, detail retrieval, album-specific review retrieval, and explicit ingestion of external albums into local storage.
The review API is centered on ReviewApiController. Read operations are public, while write operations require ROLE_USER. Responses are serialized into a stable JSON shape that includes the review itself plus minimal nested album and author information. Sensitive fields such as email addresses, passwords, and role data are intentionally excluded. This is important because the assignment criteria explicitly reward avoiding unnecessary or sensitive output.
Shared response behaviour is centralized in ApiController, which provides helper methods for successful responses and consistent error envelopes. This reduces duplication across controllers and makes the API easier to maintain. Errors are further normalized by ApiExceptionSubscriber and ApiJwtFailureSubscriber, so authentication failures, invalid tokens, missing credentials, validation errors, and unexpected server errors are all returned as JSON instead of HTML exception pages. That is a significant usability improvement for third-party clients.
Authentication and Authorization
Authentication is implemented with LexikJWTAuthenticationBundle. The login endpoint is handled through Symfony security json_login at POST /api/v1/auth/login. This design is stateless because the client submits credentials once, receives a token, and then supplies that token on later requests. The server does not need to keep session state for API consumers.
Authorization is layered on top of authentication. Access control rules in the security configuration protect the write routes, but review ownership is enforced more precisely by ReviewVoter. A normal user may edit or delete only their own review, while moderators and administrators may manage any review. This is stronger than basic route-level protection because it applies business rules at the resource level.
RESTful Design Criteria
The API satisfies several core REST criteria. First, resources are named with nouns rather than action verbs: /reviews, /reviews/{id}, /albums, and /albums/{id}/reviews. Second, HTTP methods are used according to intent: GET for retrieval, POST for creation and ingestion, DELETE for removal, and PUT/PATCH for updates. Third, the API uses JSON consistently for request and response bodies, which keeps the contract simple for clients.
The service is also stateless for protected routes because every authenticated request carries its own bearer token. Status codes are generally used correctly: 200 for successful reads and updates, 201 for creation, 204 for deletion, 401 for authentication failures, 403 for authorization failures, 404 for missing resources, and 422 for validation problems.
One limitation is that PUT and PATCH are currently routed to the same update handler. In strict REST terms, PUT would usually imply full replacement of editable state, whereas PATCH implies partial modification. The current implementation is still practical and easy for clients to consume, but it is better described as REST-oriented than perfectly REST-pure in that one area.
External API Consumption
External API consumption is implemented with Guzzle and a provider abstraction. Rather than hard-coding one service into controllers, the application uses ExternalAlbumClientInterface and an ExternalAlbumClient router that resolves the currently active provider. This allows the application to work with Last.fm, MusicBrainz, or TIDAL while keeping the rest of the code independent from provider-specific payload formats.
This abstraction is one of the strongest architectural parts of the project. LastFmClient, MusicBrainzClient, and TidalClient all normalize external responses into the same internal album contract: album ID, title, artists, genres, release date, cover image URLs, external URL, and track list. Because of this, the UI and the API can render albums without knowing which provider originally supplied the data.
The integration is also deep rather than superficial. Album search merges local and external results, attaches review aggregates, supports filtering by artist, genre, year range, and source, and can run in fast or full modes. Opening an unsaved external album detail page can automatically hydrate and persist it locally. The album page then displays cover art, release metadata, genres, tracklists, and user reviews in one place.
The latest provider addition is TIDAL. Its client uses OAuth 2.0 client credentials, caches access tokens, retries once on a 401, and normalizes JSON:API documents into the same internal shape used by the other providers. This is a non-trivial second external integration and improves the project’s depth against the assignment rubric.
Code Organization and Maintainability
The codebase is organized around separation of concerns. Controllers are thin orchestration layers. Services handle provider logic, search composition, uploads, registration toggling, and provider switching. Entities define the core data model. Event subscribers centralize error handling. Security rules and JWT behaviour are configured declaratively rather than embedded into controller code.
Maintainability is strengthened further by tests. Functional API tests cover authentication, malformed JSON, validation failures, forbidden updates, missing resources, and protection of personally identifiable information. Unit tests cover provider routing and TIDAL normalization behaviour. Although I was not able to execute PHPUnit in this shell because php is not installed locally, the presence of these tests is still relevant evidence of intended behaviour and regression control.
Limitations and Further Work
The current API is solid for the project scope, but there are clear next steps. Pagination metadata could be formalized on list endpoints. PUT and PATCH semantics could be separated more strictly. JWT refresh token support could be added for longer-lived API clients. Rate-limiting and request-throttling at the API boundary would also strengthen production readiness. Finally, album search response headers still use Last.fm-oriented metric names even though multiple providers are now supported, so those observability labels could be generalized.
Conclusion
Overall, Tonehaus meets the assignment brief with a realistic REST API, JWT-protected write operations, structured error handling, and meaningful external API consumption. The project goes beyond a minimal CRUD exercise because the consumed APIs actively shape the web application experience: they power album discovery, metadata enrichment, cover art, tracklists, and ingestion into the local review catalog. The result is a more maintainable and scalable application that treats the API as part of the system architecture rather than an isolated add-on.
References
- Module brief: Assignment 2 - RESTful APIs, Advanced Web Development, CRN 31802.
- Symfony Documentation.
- LexikJWTAuthenticationBundle documentation.
- FOSRestBundle documentation.
- NelmioApiDocBundle documentation.
- Guzzle documentation.
- Last.fm API documentation.
- MusicBrainz Web Service documentation and Cover Art Archive.
- TIDAL Open API documentation.