612 lines
18 KiB
Markdown
612 lines
18 KiB
Markdown
# 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:
|
||
|
||
```http
|
||
Authorization: Bearer <jwt-token>
|
||
```
|
||
|
||
Protected routes:
|
||
|
||
- `POST /api/v1/albums/ingest`
|
||
- `POST /api/v1/reviews`
|
||
- `PUT /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:
|
||
|
||
```json
|
||
{
|
||
"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_required`
|
||
- `invalid_credentials`
|
||
- `invalid_token`
|
||
- `token_expired`
|
||
- `validation_error`
|
||
- `album_not_found`
|
||
- `external_album_not_found`
|
||
- `forbidden`
|
||
- `not_found`
|
||
- `method_not_allowed`
|
||
- `internal_server_error`
|
||
|
||
### 4. Resource Models
|
||
|
||
#### Review Resource
|
||
|
||
```json
|
||
{
|
||
"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
|
||
|
||
```json
|
||
{
|
||
"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
|
||
|
||
```json
|
||
{
|
||
"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:251380836`
|
||
- `mbid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`
|
||
- `u_ab12cd34ef56` for user-created albums
|
||
|
||
### 5. Endpoint Reference
|
||
|
||
### `GET /api/v1`
|
||
|
||
Purpose: API index and discovery endpoint.
|
||
|
||
Auth: None
|
||
|
||
Response:
|
||
|
||
- `200 OK`
|
||
|
||
Example:
|
||
|
||
```json
|
||
{
|
||
"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:
|
||
|
||
```json
|
||
{
|
||
"email": "user@example.com",
|
||
"password": "password123"
|
||
}
|
||
```
|
||
|
||
Responses:
|
||
|
||
- `200 OK` token issued
|
||
- `401 Unauthorized` invalid credentials
|
||
|
||
Success example:
|
||
|
||
```json
|
||
{
|
||
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9..."
|
||
}
|
||
```
|
||
|
||
### `GET /api/v1/albums`
|
||
|
||
Purpose: search albums from the local catalog and the currently configured external provider.
|
||
|
||
Auth: None
|
||
|
||
Query parameters:
|
||
|
||
- `query` free-text search
|
||
- `album` album title filter
|
||
- `artist` artist filter
|
||
- `genre` genre filter
|
||
- `year_from` inclusive lower year bound
|
||
- `year_to` inclusive upper year bound
|
||
- `source` `all`, `external`, or `user`
|
||
- `limit` result limit, capped server-side
|
||
- `mode` `full` or `fast`
|
||
|
||
Example:
|
||
|
||
```http
|
||
GET /api/v1/albums?query=4%3A44&artist=JAY-Z&limit=5&mode=fast
|
||
Accept: application/json
|
||
```
|
||
|
||
Responses:
|
||
|
||
- `200 OK`
|
||
|
||
Success example:
|
||
|
||
```json
|
||
{
|
||
"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_id` must be supplied
|
||
- or `query`
|
||
- or a combination of `artist` and `album`
|
||
|
||
Example:
|
||
|
||
```json
|
||
{
|
||
"external_id": "tidal:251380836"
|
||
}
|
||
```
|
||
|
||
Responses:
|
||
|
||
- `201 Created`
|
||
- `400 Bad Request` invalid JSON or missing identifier/query
|
||
- `401 Unauthorized`
|
||
- `404 Not Found` external album not found
|
||
- `422 Unprocessable Entity` album missing metadata or tracks
|
||
|
||
Success response includes:
|
||
|
||
- `data` album payload
|
||
- `tracks` normalized track list
|
||
|
||
### `GET /api/v1/albums/{id}`
|
||
|
||
Purpose: return one album by public ID.
|
||
|
||
Auth: None
|
||
|
||
Path parameter:
|
||
|
||
- `id` public album identifier string
|
||
|
||
Responses:
|
||
|
||
- `200 OK`
|
||
- `404 Not Found`
|
||
- `422 Unprocessable Entity` album payload exists upstream but is not ingestable
|
||
|
||
Success example:
|
||
|
||
```json
|
||
{
|
||
"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 OK`
|
||
- `404 Not Found`
|
||
|
||
Example response:
|
||
|
||
```json
|
||
{
|
||
"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:
|
||
|
||
- `limit` integer, clamped to `1-100`, default `50`
|
||
|
||
Responses:
|
||
|
||
- `200 OK`
|
||
|
||
Example:
|
||
|
||
```http
|
||
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 OK`
|
||
- `404 Not Found`
|
||
|
||
### `POST /api/v1/reviews`
|
||
|
||
Purpose: create a review for an existing album.
|
||
|
||
Auth: JWT required
|
||
|
||
Request body:
|
||
|
||
```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
|
||
}
|
||
```
|
||
|
||
Validation rules:
|
||
|
||
- `album_id` must resolve to an existing album with a track list
|
||
- `title` is required
|
||
- `content` is required and must be at least 20 characters
|
||
- `rating` must be between 1 and 10
|
||
|
||
Responses:
|
||
|
||
- `201 Created`
|
||
- `400 Bad Request` invalid JSON
|
||
- `401 Unauthorized`
|
||
- `404 Not Found` album not found
|
||
- `422 Unprocessable Entity` validation 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:
|
||
|
||
- `title`
|
||
- `content`
|
||
- `rating`
|
||
|
||
Validation:
|
||
|
||
- at least one updatable field is required
|
||
- `title` cannot be empty
|
||
- `content` cannot be empty and must be at least 20 characters
|
||
- `rating` must be between 1 and 10
|
||
- only the review author, moderator, or admin may modify the review
|
||
|
||
Responses:
|
||
|
||
- `200 OK`
|
||
- `400 Bad Request`
|
||
- `401 Unauthorized`
|
||
- `403 Forbidden`
|
||
- `404 Not Found`
|
||
- `422 Unprocessable Entity`
|
||
|
||
Example:
|
||
|
||
```json
|
||
{
|
||
"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:
|
||
|
||
```json
|
||
{
|
||
"rating": 10
|
||
}
|
||
```
|
||
|
||
Responses:
|
||
|
||
- `200 OK`
|
||
- `400 Bad Request`
|
||
- `401 Unauthorized`
|
||
- `403 Forbidden`
|
||
- `404 Not Found`
|
||
- `422 Unprocessable Entity`
|
||
|
||
### `DELETE /api/v1/reviews/{id}`
|
||
|
||
Purpose: delete a review.
|
||
|
||
Auth: JWT required
|
||
|
||
Authorization:
|
||
|
||
- author of the review
|
||
- moderator
|
||
- admin
|
||
|
||
Responses:
|
||
|
||
- `204 No Content`
|
||
- `401 Unauthorized`
|
||
- `403 Forbidden`
|
||
- `404 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
|
||
|
||
1. Module brief: *Assignment 2 - RESTful APIs*, Advanced Web Development, CRN 31802.
|
||
2. Symfony Documentation.
|
||
3. LexikJWTAuthenticationBundle documentation.
|
||
4. FOSRestBundle documentation.
|
||
5. NelmioApiDocBundle documentation.
|
||
6. Guzzle documentation.
|
||
7. Last.fm API documentation.
|
||
8. MusicBrainz Web Service documentation and Cover Art Archive.
|
||
9. TIDAL Open API documentation.
|