Files
tonehaus/docs/assignment2-report.md
2026-04-02 13:46:07 +01:00

18 KiB
Raw Permalink Blame History

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/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:

{
  "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

{
  "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: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:

{
  "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 OK token issued
  • 401 Unauthorized invalid 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:

  • 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:

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_id must be supplied
  • or query
  • or a combination of artist and album

Example:

{
  "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:

{
  "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:

{
  "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:

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:

{
  "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:

{
  "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 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.