{"id":22372219,"url":"https://github.com/keidsid/dicoding-back-end-intermediate","last_synced_at":"2025-03-26T17:18:07.542Z","repository":{"id":114542750,"uuid":"591987737","full_name":"KeidsID/dicoding-back-end-intermediate","owner":"KeidsID","description":"Project task from dicoding.com intermediate Back-End class","archived":false,"fork":false,"pushed_at":"2023-02-27T07:25:56.000Z","size":994,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-01-31T22:09:07.708Z","etag":null,"topics":["hapi-js","node-js","postgre-sql","rabbitmq-producer","redis"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/KeidsID.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-01-22T15:26:58.000Z","updated_at":"2023-06-09T13:43:23.000Z","dependencies_parsed_at":null,"dependency_job_id":"9fa57753-b0be-47c4-9df8-0ea33897d5ad","html_url":"https://github.com/KeidsID/dicoding-back-end-intermediate","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/KeidsID%2Fdicoding-back-end-intermediate","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/KeidsID%2Fdicoding-back-end-intermediate/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/KeidsID%2Fdicoding-back-end-intermediate/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/KeidsID%2Fdicoding-back-end-intermediate/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/KeidsID","download_url":"https://codeload.github.com/KeidsID/dicoding-back-end-intermediate/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":245699261,"owners_count":20657987,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["hapi-js","node-js","postgre-sql","rabbitmq-producer","redis"],"created_at":"2024-12-04T20:34:19.540Z","updated_at":"2025-03-26T17:18:07.508Z","avatar_url":"https://github.com/KeidsID.png","language":"JavaScript","readme":"[class-link]: https://www.dicoding.com/academies/271\n[pm-v1]: https://github.com/dicodingacademy/a271-backend-menengah-labs/raw/099-shared-files/03-submission-content/01-open-music-api-v1/OpenMusic%20API%20V1%20Test.zip\n[pm-v2]: https://github.com/dicodingacademy/a271-backend-menengah-labs/raw/099-shared-files/03-submission-content/02-open-music-api-v2/OpenMusic%20API%20V2%20Test.zip\n[pm-v3]: https://github.com/dicodingacademy/a271-backend-menengah-labs/raw/099-shared-files/03-submission-content/03-open-music-api-v3/OpenMusic%20API%20V3%20Test.zip\n[my-consumer]: https://github.com/KeidsID/dicoding-back-end-intermediate-mq-consumer\n\n# dicoding-back-end-intermediate\n\nProject task from [dicoding.com Back-End Intermediate Class][class-link].\n\nThis project is a learning outcome through the platform dicoding.com. The aim is to enable students to create a RestfulAPI rich in features, including data validations, database relations, auth with JWT Token, MQ, File Storage, and Server-Side Cache.\n\nLink to MQ Consumer: [dicoding-back-end-intermediate-mq-consumer][my-consumer]\n\n### **Postman collections and envs for testing this project:**\n\n- [API.v1 test zip][pm-v1].\n- [API.v2 test zip][pm-v2].\n- [API.v3 test zip][pm-v3].\n\n### **List of README.md Contents:**\n\n- [Project Set Up](#project-set-up)\n- [TO DO API.v1](#to-do-apiv1)\n- [TO DO API.v1 Details](#to-do-apiv1-details)\n- [TO DO API.v2](#to-do-apiv2)\n- [TO DO API.v2 Details](#to-do-apiv2-details)\n- [TO DO API.v3](#to-do-apiv3)\n- [TO DO API.v3 Details](#to-do-apiv3-details)\n\n# Project Set Up\n\n- Create the \"**.env**\" file with the following data below:\n\n  ```sh\n  # Server config\n  HOST=localhost\n  PORT=\u003cdesired port\u003e\n\n  # node-postgres config\n  PGUSER=\u003cyour psql user\u003e\n  PGHOST=\u003cyour psql host\u003e\n  PGPASSWORD=\u003cyour psql password\u003e\n  PGDATABASE=\u003cyour psql database name\u003e\n  PGPORT=\u003cyour psql port\u003e\n\n  # JWT config\n  ACCESS_TOKEN_KEY=\u003crandom string\u003e\n  REFRESH_TOKEN_KEY=\u003crandom string\u003e\n  ACCESS_TOKEN_AGE=\u003cduration in ms\u003e\n\n  # RabbitMQ config\n  RABBITMQ_SERVER=\u003cyour RabbitMQ server\u003e\n\n  # Redis config\n  REDIS_SERVER=\u003cyour Redis host\u003e\n  ```\n\n- Then run this command\n  ```\n  npm install\n  npm run pgm up\n  ```\n\n**Note**:\n\nIf you do TRUNCATE tables, make sure to re-add default album data (id: album-unknown) into \"**albums**\" table.\n\n```sql\nINSERT INTO albums\nVALUES('album-unknown', 'Unknown', 1945)\n```\n\n# TO DO API.v1\n\n[See TO DO API.v1 Details](#to-do-apiv1-details).\n\n## Mandatory Tasks\n\n- [x] Albums endpoint.\n- [x] Songs endpoint.\n- [x] Data validation.\n- [x] Error handling.\n- [x] Using Database.\n\n## Optional Tasks\n\n- [x] \"/albums/{id}\" endpoint response array of Song on Album too.\n- [x] Query params for songs endpoint.\n\n# TO DO API.v2\n\n[See TO DO API.v2 Details](#to-do-apiv2-details).\n\n## Mandatory Tasks\n\n- [x] Registration and Authentication Users.\n- [x] Playlist endpoint.\n- [x] Implement Foreign Key on Database Tables.\n- [x] Data validation for new endpoints.\n- [x] Error handling for new endpoints.\n- [x] Keep features from API.v1.\n\n## Optional Tasks\n\n- [x] Collaborations on Playlists Feature.\n- [x] Activities endpoint for Playlist Log History.\n- [x] Keep optional features from API.v1.\n\n# TO DO API.v3\n\n[See TO DO API.v3 Details](#to-do-apiv3-details).\n\n## Mandatory Tasks\n\n- [x] Export playlist feature. MQ Consumer: [dicoding-back-end-intermediate-mq-consumer][my-consumer]\n- [x] Upload album cover feature.\n- [x] Like and Unlike albums feature.\n- [x] Server-Side cache.\n- [x] Keep features from API.v2.\n\n## Optional Tasks\n\nNo optional task yay.\n\n# TO DO API.v1 Details\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"#dicoding-back-end-intermediate\"\u003eBack to Top\u003c/a\u003e |\n  \u003ca href=\"#optional-tasks-3\"\u003eOptional Tasks\u003c/a\u003e |\n  \u003ca href=\"#to-do-apiv2-details\"\u003eTO DO API.v2 Details\u003c/a\u003e |\n  \u003ca href=\"#to-do-apiv3-details\"\u003eTO DO API.v3 Details\u003c/a\u003e\n\u003c/p\u003e\n\n## Mandatory Tasks\n\n### **1. Albums Endpoint**\n\n![albums-structure](readme-assets/struktur-api-album.png)\n\n\u003cp align=\"center\"\u003e*any: Any \u003cb\u003estring,\u003c/b\u003e but not \u003cb\u003enull\u003c/b\u003e.\u003c/p\u003e\n\nAlbum obj structure:\n\n```json\n{\n  \"id\": \"album-\u003cunique-id-here\u003e\",\n  \"name\": \"lorem ipsum\",\n  \"year\": 2012\n}\n```\n\n### **2. Songs Endpoint**\n\n![songs-structure](readme-assets/struktur-api-song.png)\n\n\u003cp align=\"center\"\u003e*any: Any \u003cb\u003estring,\u003c/b\u003e but not \u003cb\u003enull\u003c/b\u003e.\u003c/p\u003e\n\u003cp align=\"center\"\u003e*?: Can be \u003cb\u003enull\u003c/b\u003e or \u003cb\u003eundefined\u003c/b\u003e.\u003c/p\u003e\n\nSong obj structures:\n\n- Main structure.\n\n```json\n{\n  \"id\": \"song-\u003cunique-id-here\u003e\",\n  \"title\": \"Lorem Ipsum\",\n  \"year\": 2008,\n  \"performer\": \"John Doe\",\n  \"genre\": \"Indie\",\n  \"duration\": 120,\n  \"albumId\": \"album-id\"\n}\n```\n\n- Only for GET /songs endpoint.\n\n```json\n{\n  \"id\": \"song-\u003cunique-id-here\u003e\",\n  \"title\": \"Life in Technicolor\",\n  \"performer\": \"Coldplay\"\n}\n```\n\n### **3. Data Validations**\n\n- POST /albums\n\n  - **name**: string, required.\n  - **year**: number, required.\n\n- PUT /albums\n\n  - **name**: string, required.\n  - **year**: number, required.\n\n- POST /songs\n\n  - **title**: string, required.\n  - **year**: number, required.\n  - **genre**: string, required.\n  - **performer**: string, required.\n  - **duration**: number.\n  - **albumId**: string.\n\n- PUT /songs\n  - **title**: string, required.\n  - **year**: number, required.\n  - **genre**: string, required.\n  - **performer**: string, required.\n  - **duration**: number.\n  - **albumId**: string.\n\n### **4. Error Handling**\n\n- Validation Error Response:\n  - status code: **400 (Bad Request)**\n  - response body:\n    ```json\n    {\n      \"status\": \"fail\",\n      \"message\": \u003cany but not null\u003e,\n    }\n    ```\n- Not Found Error Response:\n  - status code: **404 (Not Found)**\n  - response body:\n    ```json\n    {\n      \"status\": \"fail\",\n      \"message\": \u003cany but not null\u003e,\n    }\n    ```\n- Server Error Response:\n  - status code: **500 (Internal Server Error)**\n  - response body:\n    ```json\n    {\n      \"status\": \"error\",\n      \"message\": \u003cany but not null\u003e,\n    }\n    ```\n\n### **5. Using Database**\n\n- Use PostgreSQL to store data. So the data will not be lost if the server is down.\n\n- Use [dotenv](https://www.npmjs.com/package/dotenv) to manage environment variables that store credentials for accessing database.\n\n## Optional Tasks\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"#dicoding-back-end-intermediate\"\u003eBack to Top\u003c/a\u003e |\n  \u003ca href=\"#to-do-apiv1-details\"\u003eBack to Mandatory Tasks\u003c/a\u003e |\n  \u003ca href=\"#to-do-apiv3-details\"\u003eTO DO API.v3 Details\u003c/a\u003e\n\u003c/p\u003e\n\n### **1. \"/albums/{id}\" endpoint response array of Song on Album too**\n\nExample:\n\n```json\n{\n  \"status\": \"success\",\n  \"data\": {\n    \"album\": {\n      \"id\": \"album-Mk8AnmCp210PwT6B\",\n      \"name\": \"Viva la Vida\",\n      \"year\": 2008,\n      \"songs\": [\n        {\n          \"id\": \"song-Qbax5Oy7L8WKf74l\",\n          \"title\": \"Life in Technicolor\",\n          \"performer\": \"Coldplay\"\n        }\n      ]\n    }\n  }\n}\n```\n\n### **2. Query Params for Songs Endpoint**\n\nMake the **GET /songs** support query params for searching.\n\n- **?title**: Search song based on title.\n- **?performer**: Search song based on performer.\n\n**Note**: Both queries can be combined ( \".../songs?title=lmao\u0026performer=pisan\" )\n\n# TO DO API.v2 Details\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"#dicoding-back-end-intermediate\"\u003eBack to Top\u003c/a\u003e |\n  \u003ca href=\"#optional-tasks-4\"\u003eOptional Tasks\u003c/a\u003e |\n  \u003ca href=\"#to-do-apiv1-details\"\u003eTO DO API.v1 Details\u003c/a\u003e |\n  \u003ca href=\"#to-do-apiv3-details\"\u003eTO DO API.v3 Details\u003c/a\u003e\n\u003c/p\u003e\n\n## Mandatory Tasks\n\n### **1. Registration and Authentication Users**\n\n![auth-structure](readme-assets/struktur-api-auth.png)\n\n\u003cp align=\"center\"\u003e*any: Any \u003cb\u003estring,\u003c/b\u003e but not \u003cb\u003enull\u003c/b\u003e.\u003c/p\u003e\n\n**Conditions**:\n\n- **Username** must unique.\n- Using JWT token for Auth.\n- JWT token payload contains **userId**.\n- JWT token secret key value stored on envs as **ACCESS_TOKEN_KEY** and **REFRESH_TOKEN_KEY**.\n\n### **2. Playlist endpoint**\n\n![playlists-structure](readme-assets/struktur-api-playlists.png)\n\n\u003cp align=\"center\"\u003e*any: Any \u003cb\u003estring,\u003c/b\u003e but not \u003cb\u003enull\u003c/b\u003e.\u003c/p\u003e\n\n**Conditions**:\n\n- **Restrict** endpoint (Need \"access token\" to access).\n- **GET /playlists** returns owned playlists (And collab playlists if exist).\n- Collaborator (if exist) can access **songs** (add, get, and delete) from playlist, but only owners can delete their own playlists.\n- Only valid **songId** can be add/delete to/from playlist.\n\n**Responses**:\n\n- GET /playlists\n\n```json\n{\n  \"status\": \"success\",\n  \"data\": {\n    \"playlists\": [\n      {\n        \"id\": \"playlist-Qbax5Oy7L8WKf74l\",\n        \"name\": \"Lagu Indie Hits Indonesia\",\n        \"username\": \"dicoding\"\n      },\n      {\n        \"id\": \"playlist-lmA4PkM3LseKlkmn\",\n        \"name\": \"Lagu Untuk Membaca\",\n        \"username\": \"dicoding\"\n      }\n    ]\n  }\n}\n```\n\n- GET /playlists/{id}/songs\n\n```json\n{\n  \"status\": \"success\",\n  \"data\": {\n    \"playlist\": {\n      \"id\": \"playlist-Mk8AnmCp210PwT6B\",\n      \"name\": \"My Favorite Coldplay\",\n      \"username\": \"dicoding\",\n      \"songs\": [\n        {\n          \"id\": \"song-Qbax5Oy7L8WKf74l\",\n          \"title\": \"Life in Technicolor\",\n          \"performer\": \"Coldplay\"\n        },\n        {\n          \"id\": \"song-poax5Oy7L8WKllqw\",\n          \"title\": \"Centimeteries of London\",\n          \"performer\": \"Coldplay\"\n        },\n        {\n          \"id\": \"song-Qalokam7L8WKf74l\",\n          \"title\": \"Lost!\",\n          \"performer\": \"Coldplay\"\n        }\n      ]\n    }\n  }\n}\n```\n\n**Obj Playlist for Database**:\n\n```json\n{\n  \"id\": \"playlist-Qbax5Oy7L8WKf74l\",\n  \"name\": \"Lagu Indie Hits Indonesia\",\n  \"owner\": \"user-Qbax5Oy7L8WKf74l\"\n}\n```\n\n### **3. Implement Foreign Key**\n\n- Table **songs** related to **albums**.\n- Table **playlists** related to **users**.\n- etc.\n\n### **4. Data Validation**\n\n- POST /users\n\n  - **username**: string, required.\n  - **password**: string, required.\n  - **fullname**: string, required.\n\n- POST /authentications\n\n  - **username**: string, required.\n  - **password**: string, required.\n\n- PUT /authentications\n\n  - **refreshToken**: string, required.\n\n- DELETE /authentications\n\n  - **refreshToken**: string, required.\n\n- POST /playlists\n\n  - **name**: string, required.\n\n- POST /playlists/{playlistId}/songs\n\n  - **songId**: string, required.\n\n### **5. Error Handling**\n\nThe previous error handler is still in use, but there is a new handler for Auth.\n\n- Authorization Error:\n  - status code: **401 (Unauthorized)**\n  - response body:\n    ```json\n    {\n      \"status\": \"fail\",\n      \"message\": \u003cAny, but not null\u003e\n    }\n    ```\n- Restrict Error:\n  - status code: **403 (Forbidden)**\n  - response body:\n    ```json\n    {\n      \"status\": \"fail\",\n      \"message\": \u003cAny, but not null\u003e\n    }\n    ```\n\n### **6. Keep Features from API.v1**\n\n- Albums Feature.\n- Songs Feature.\n- Validations for Songs and Albums Endpoints.\n\n## Optional Tasks\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"#dicoding-back-end-intermediate\"\u003eBack to Top\u003c/a\u003e |\n  \u003ca href=\"#to-do-apiv2-details\"\u003eBack to Mandatory Tasks\u003c/a\u003e |\n  \u003ca href=\"#to-do-apiv1-details\"\u003eTO DO API.v1 Details\u003c/a\u003e |\n  \u003ca href=\"#to-do-apiv3-details\"\u003eTO DO API.v3 Details\u003c/a\u003e\n\u003c/p\u003e\n\n### **1. Playlists Collaboration Feature**\n\n![collaborations-structure](readme-assets/struktur-api-collaborations.png)\n\n\u003cp align=\"center\"\u003e*any: Any \u003cb\u003estring,\u003c/b\u003e but not \u003cb\u003enull\u003c/b\u003e.\u003c/p\u003e\n\n**Conditions**:\n\n- Only the owner of the playlist can add or remove collaborators from the playlist.\n\n**Collaborator access rights**:\n\n- Collaborated playlists also shown on \"GET /playlists\" endpoint data.\n- Can add/get/delete songs to/from playlist.\n- Can see playlist activities too (If already implemented).\n\n### **2. Activities endpoint for Playlist Log History**\n\nThis feature is used to record the history of **adding and removing** **songs from playlists** by users or collaborators.\n\nEndpoint: **GET /playlists/{id}/activities**\n\nResponse example:\n\n- Status Code: 200\n- Body:\n  ```json\n  {\n    \"status\": \"success\",\n    \"data\": {\n      \"playlistId\": \"playlist-Mk8AnmCp210PwT6B\",\n      \"activities\": [\n        {\n          \"username\": \"dicoding\",\n          \"title\": \"Life in Technicolor\",\n          \"action\": \"add\",\n          \"time\": \"2021-09-13T08:06:20.600Z\"\n        },\n        {\n          \"username\": \"dicoding\",\n          \"title\": \"Centimeteries of London\",\n          \"action\": \"add\",\n          \"time\": \"2021-09-13T08:06:39.852Z\"\n        },\n        {\n          \"username\": \"dimasmds\",\n          \"title\": \"Life in Technicolor\",\n          \"action\": \"delete\",\n          \"time\": \"2021-09-13T08:07:01.483Z\"\n        }\n      ]\n    }\n  }\n  ```\n\n### **3. Keep optional features from API.v1**\n\n- Songs list from album detail.\n- Query param for search songs.\n\n# TO DO API.v3 Details\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"#dicoding-back-end-intermediate\"\u003eBack to Top\u003c/a\u003e |\n  \u003ca href=\"#to-do-apiv1-details\"\u003eTO DO API.v1 Details\u003c/a\u003e |\n  \u003ca href=\"#to-do-apiv2-details\"\u003eTO DO API.v2 Details\u003c/a\u003e\n\u003c/p\u003e\n\n### **1. Export Playlist Feature**\n\n**Server Route**:\n\n- Endpoint: **POST /export/playlists/{id}**\n- Body Req (JSON):\n  ```json\n  {\n    targetEmail: \u003cstring\u003e\n  }\n  ```\n\n**Server Response**:\n\n- Status code: **201**\n- Body Res:\n  ```json\n  {\n    \"status\": \"success\",\n    \"message\": \u003cany string but not null\u003e\n  }\n  ```\n\n**Conditions**:\n\n- Using RabbitMQ.\n  - The RabbitMQ server host value must be stored in the environment variable as **RABBITMQ_SERVER**.\n- Only owner can export his/her own playlists.\n- The export result is in JSON format file.\n\n  ```json\n  {\n    \"playlist\": {\n      \"id\": \"playlist-Mk8AnmCp210PwT6B\",\n      \"name\": \"My Favorite Coldplay Song\",\n      \"songs\": [\n        {\n          \"id\": \"song-Qbax5Oy7L8WKf74l\",\n          \"title\": \"Life in Technicolor\",\n          \"performer\": \"Coldplay\"\n        },\n        {\n          \"id\": \"song-poax5Oy7L8WKllqw\",\n          \"title\": \"Centimeteries of London\",\n          \"performer\": \"Coldplay\"\n        },\n        {\n          \"id\": \"song-Qalokam7L8WKf74l\",\n          \"title\": \"Lost!\",\n          \"performer\": \"Coldplay\"\n        }\n      ]\n    }\n  }\n  ```\n\n- Also create the consumer source code in a new project, not in this one. With condition below.\n  - Send the export result through email using [nodemailer](https://nodemailer.com/).\n    - SMTP credentials value must be stored in the environment variable as **MAIL_ADDRESS** and **MAIL_PASSWORD**.\n    - SMTP server value must be stored in the environment variable as **MAIL_HOST** and **MAIL_PORT**.\n\n### **2. Upload Album Cover Feature**\n\n**Server Route**:\n\n- Endpoint: **POST /albums/{id}/covers**\n- Body Req:\n  ```json\n  {\n    \"cover\": \u003cimage file\u003e\n  }\n  ```\n\n**Server Response**:\n\n- Status code: **201**\n- Body Res:\n  ```json\n  {\n    \"status\": \"success\",\n    \"message\": \u003cany string but not null\u003e\n  }\n  ```\n\n**Conditions**:\n\n- Only [MIME types of images](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#image_types) are allowed to upload.\n- File max size is **512000 Bytes**.\n- Local storage or AWS S3 are allowed to use.\n  - When using AWS S3 Bucket, the bucket name must be stored in the environment variable as **AWS_BUCKET_NAME**.\n- **GET /albums/{id}** must response uploaded **coverUrl** too.\n\n  ```json\n  {\n    \"status\": \"success\",\n    \"data\": {\n      \"album\": {\n        \"id\": \"album-Mk8AnmCp210PwT6B\",\n        \"name\": \"Viva la Vida\",\n        \"year\": 2018\n        \"coverUrl\": \"http://....\"\n      }\n    }\n  }\n  ```\n\n  - coverUrl response an uploaded image.\n  - If cover not uploaded yet, then response null on coverUrl instead.\n  - When uploadig cover to an album that already has cover, the old cover are replaced.\n\n### **3. Like and Unlike Albums Feature**\n\n![album-likes-structure](readme-assets/struktur-api-album-likes.png)\n\n\u003cp align=\"center\"\u003e*any: Any \u003cb\u003estring,\u003c/b\u003e but not \u003cb\u003enull\u003c/b\u003e.\u003c/p\u003e\n\n**Conditions**:\n\n- Liking or unliking an album is a strict resource so authentication is required to access it. This aims to find out whether the user has liked the album.\n- If the user hasn't liked the album, then the \"**POST action /albums/{id}/likes**\" is to like the album. If the user has already liked the album, then the action is unliking.\n\n### **4. Server-Side Cache**\n\n- Implement a server-side cache on the number of likes on an album (**GET /albums/{id}/likes**).\n- Cache expiration time: 30 minutes.\n- The response header for cache result is \"**X-Data-Source**\" with \"cache\" value.\n- Cache should be deleted every time there is a change in the number of likes on an album.\n- Must use Redis (Memurai for Windows) memory caching engine.\n  - Redis **Host** value must be stored in the environment variable as **REDIS_SERVER**.\n\n### **5. Keep Features from API.v2**\n\nOf course 🙄.\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"#dicoding-back-end-intermediate\"\u003eBack to Top\u003c/a\u003e |\n  \u003ca href=\"#to-do-apiv1-details\"\u003eTO DO API.v1 Details\u003c/a\u003e |\n  \u003ca href=\"#to-do-apiv2-details\"\u003eTO DO API.v2 Details\u003c/a\u003e |\n  \u003ca href=\"#to-do-apiv3-details\"\u003eTO DO API.v3 Details\u003c/a\u003e\n\u003c/p\u003e\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkeidsid%2Fdicoding-back-end-intermediate","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkeidsid%2Fdicoding-back-end-intermediate","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkeidsid%2Fdicoding-back-end-intermediate/lists"}