{"id":28887389,"url":"https://github.com/kyyril/backend-blogs","last_synced_at":"2026-05-04T20:36:40.811Z","repository":{"id":295800336,"uuid":"991234380","full_name":"kyyril/backend-blogs","owner":"kyyril","description":"Blog API Backend A RESTful API backend for a blog application built with TypeScript, Express.js, Postgresql and Prisma ORM.","archived":false,"fork":false,"pushed_at":"2025-06-13T13:16:23.000Z","size":221,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-06-20T23:44:56.540Z","etag":null,"topics":["cloudinary","expressjs","google-auth","jwt","postgresql","prisma"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/kyyril.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"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,"zenodo":null}},"created_at":"2025-05-27T10:22:28.000Z","updated_at":"2025-06-13T13:16:26.000Z","dependencies_parsed_at":"2025-06-20T23:37:52.914Z","dependency_job_id":"af32b463-97ff-4f01-ae9c-3467cc200699","html_url":"https://github.com/kyyril/backend-blogs","commit_stats":null,"previous_names":["kyyril/backend-blogs"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/kyyril/backend-blogs","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kyyril%2Fbackend-blogs","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kyyril%2Fbackend-blogs/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kyyril%2Fbackend-blogs/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kyyril%2Fbackend-blogs/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kyyril","download_url":"https://codeload.github.com/kyyril/backend-blogs/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kyyril%2Fbackend-blogs/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":264341082,"owners_count":23593293,"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":["cloudinary","expressjs","google-auth","jwt","postgresql","prisma"],"created_at":"2025-06-20T23:37:34.185Z","updated_at":"2026-05-04T20:36:35.784Z","avatar_url":"https://github.com/kyyril.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Blog API Backend\n\nA RESTful API backend for a blog application built with TypeScript, Express.js, Supabase, and Prisma ORM.\n\n## Features\n\n- **User Authentication \u0026 Profiles**\n\n  - Google OAuth authentication\n  - View user profiles with stats\n  - Follow/unfollow other users\n\n- **Blog Management**\n\n  - Create, read, update, and delete blog posts\n  - Upload blog images to Cloudinary\n  - View all blogs with pagination\n  - Search blogs by query\n  - Filter blogs by category\n  - Track blog views and engagement\n\n- **Social Features**\n\n  - Follow other bloggers\n  - View blogger's posts\n  - Comment on blog posts\n  - View follower/following counts\n\n- **Analytics**\n  - Blog view counts\n  - Follower statistics\n\n## Tech Stack\n\n- **Backend**: Node.js, Express.js, TypeScript\n- **Database**: PostgreSQL (via Supabase)\n- **ORM**: Prisma\n- **Authentication**: Google OAuth\n- **File Storage**: Cloudinary\n- **Validation**: Express Validator\n\n## API Endpoints\n\n### Authentication\n\n#### Google Authentication\n\n- **URL**: `/api/auth/google`\n- **Method**: `POST`\n- **Body**:\n\n```json\n{\n  \"token\": \"google-id-token\"\n}\n```\n\n- **Response**: User object\n\n---\n\n## Authentication\n\n### Google Authentication\n\nThis section details the Google authentication flow, allowing users to sign in using their Google accounts.\n\n- **URL**: `/api/auth/google`\n- **Method**: `POST`\n- **Body**:\n\n  ```json\n  {\n    \"token\": \"google-id-token\"\n  }\n  ```\n\n  The `token` field should contain the ID token obtained from the Google Sign-In client.\n\n- **Response**:\n\n  ```json\n  {\n    \"message\": \"Google authentication successful\",\n    \"user\": {\n      \"id\": \"user-id\",\n      \"name\": \"User Name\",\n      \"email\": \"user@example.com\",\n      \"bio\": \"User bio (if available)\",\n      \"avatar\": \"URL to user's avatar\"\n    }\n  }\n  ```\n\n  Upon successful authentication, the server sets `access_token` and `refresh_token` as HTTP-only cookies and returns the authenticated user object.\n\n### Get Authenticated User\n\nThis endpoint allows a client to retrieve information about the currently authenticated user.\n\n- **URL**: `/api/auth/me`\n- **Method**: `GET`\n- **Authentication**: Requires a valid `access_token` cookie.\n- **Response**:\n\n  ```json\n  {\n    \"user\": {\n      \"id\": \"user-id\",\n      \"name\": \"User Name\",\n      \"email\": \"user@example.com\",\n      \"bio\": \"User bio (if available)\",\n      \"avatar\": \"URL to user's avatar\"\n    }\n  }\n  ```\n\n  Returns the user object if authenticated, otherwise a `401 Unauthorized` error.\n\n### Refresh Access Token\n\nThis endpoint allows clients to obtain a new access token using a valid refresh token when the current access token expires.\n\n- **URL**: `/api/auth/refresh-token`\n- **Method**: `POST`\n- **Authentication**: Requires a valid `refresh_token` cookie.\n- **Response**:\n\n  ```json\n  {\n    \"message\": \"Token refreshed successfully\",\n    \"user\": {\n      \"id\": \"user-id\",\n      \"name\": \"User Name\",\n      \"email\": \"user@example.com\",\n      \"bio\": \"User bio (if available)\",\n      \"avatar\": \"URL to user's avatar\"\n    }\n  }\n  ```\n\n  Upon successful token refresh, a new `access_token` cookie is set, and the user object is returned. If the refresh token is invalid or expired, both `access_token` and `refresh_token` cookies are cleared, and a `401 Unauthorized` error is returned.\n\n### Logout\n\nThis endpoint logs out the user by clearing the authentication cookies.\n\n- **URL**: `/api/auth/logout`\n- **Method**: `POST`\n- **Response**:\n\n  ```json\n  {\n    \"message\": \"Logout successful\"\n  }\n  ```\n\n  Clears both `access_token` and `refresh_token` HTTP-only cookies.\n\n---\n\n### Blogs\n\n#### Create Blog\n\n- **URL**: `/api/blogs`\n- **Method**: `POST`\n- **Auth**: Required\n- **Body**: Form data with the following fields:\n\n```json\n{\n  \"title\": \"Getting Started with Next.js 15 and App Router\",\n  \"description\": \"Learn how to build modern web applications with Next.js 15 and its revolutionary App Router architecture.\",\n  \"content\": \"# Getting Started with Next.js 15 and App Router\\n\\nNext.js has revolutionized...\",\n  \"categories\": [\"Web Development\", \"React\"],\n  \"tags\": [\"nextjs\", \"react\", \"typescript\", \"app-router\"],\n  \"readingTime\": 5,\n  \"featured\": true,\n  \"image\": \"[File Upload]\"\n}\n```\n\n#### Update Blog\n\n- **URL**: `/api/blogs/blog/:id`\n- **Method**: `PUT`\n- **Auth**: Required\n- **Body**: Same as Create Blog\n\n#### Delete Blog\n\n- **URL**: `/api/blogs/blog/:id`\n- **Method**: `DELETE`\n- **Auth**: Required\n\n#### Get All Blogs\n\n- **URL**: `/api/blogs?page=1\u0026limit=10`\n- **Method**: `GET`\n\n#### Get Blog by ID\n\n- **URL**: `/api/blogs/blog/:id`\n- **Method**: `GET`\n- **Response**:\n\n```json\n{\n  \"id\": \"blog-uuid\",\n  \"title\": \"Getting Started with Next.js 15\",\n  \"slug\": \"getting-started-with-nextjs-15\",\n  \"description\": \"Learn how to build modern web applications...\",\n  \"content\": \"# Getting Started with Next.js 15...\",\n  \"image\": \"https://example.com/blog-image.jpg\",\n  \"date\": \"2024-01-20T12:00:00Z\",\n  \"readingTime\": 5,\n  \"featured\": true,\n  \"viewCount\": 1250,\n  \"likeCount\": 42,\n  \"bookmarkCount\": 15,\n  \"author\": {\n    \"id\": \"user-uuid\",\n    \"name\": \"Jane Smith\",\n    \"bio\": \"Frontend Developer\",\n    \"avatar\": \"https://example.com/avatar.jpg\"\n  },\n  \"categories\": [\"Web Development\", \"React\"],\n  \"tags\": [\"nextjs\", \"react\", \"typescript\"],\n  \"liked\": false,\n  \"bookmarked\": false\n}\n```\n\n#### Get Blog by Slug\n\n- **URL**: `/api/blogs/:slug`\n- **Method**: `GET`\n\n#### Search Blogs\n\n- **URL**: `/api/blogs/search?query=next.js\u0026page=1\u0026limit=10`\n- **Method**: `GET`\n\n#### Get Blogs by Category\n\n- **URL**: `/api/blogs/category/:category?page=1\u0026limit=10`\n- **Method**: `GET`\n\n#### Get Blogs by Tags\n\n- **URL**: `/api/blogs/tags/:tags?page=1\u0026limit=10`\n- **Method**: `GET`\n\n#### Record Blog View\n\n- **URL**: `/api/blogs/blog/:id/view`\n- **Method**: `POST`\n- **Auth**: Required\n\n#### Like Blog\n\nToggles the like status of a blog for the authenticated user.\n\n- **URL**: `/api/blogs/blog/:id/like`\n- **Method**: `POST`\n- **Auth**: Required\n- **Response**:\n\n```json\n{\n  \"message\": \"Blog liked successfully\",\n  \"liked\": true,\n  \"likeCount\": 42\n}\n```\n\nOr when unliking:\n\n```json\n{\n  \"message\": \"Blog unliked successfully\",\n  \"liked\": false,\n  \"likeCount\": 41\n}\n```\n\n#### Bookmark Blog\n\nToggles the bookmark status of a blog for the authenticated user.\n\n- **URL**: `/api/blogs/blog/:id/bookmark`\n- **Method**: `POST`\n- **Auth**: Required\n- **Response**:\n\n```json\n{\n  \"message\": \"Blog bookmarked successfully\",\n  \"bookmarked\": true,\n  \"bookmarkCount\": 15\n}\n```\n\nOr when removing bookmark:\n\n```json\n{\n  \"message\": \"Blog bookmark removed successfully\",\n  \"bookmarked\": false,\n  \"bookmarkCount\": 14\n}\n```\n\n#### Get Blog Interaction Status\n\nReturns the current user's interaction status with a blog (likes and bookmarks).\n\n- **URL**: `/api/blogs/blog/:id/interaction`\n- **Method**: `GET`\n- **Auth**: Required\n- **Response**:\n\n```json\n{\n  \"liked\": true,\n  \"bookmarked\": false,\n  \"likeCount\": 42,\n  \"bookmarkCount\": 15\n}\n```\n\n### Users\n\n#### Get User Profile\n\n- **URL**: `/api/users/:userId`\n- **Method**: `GET`\n- **Auth**: Optional\n- **Response**:\n\n````json\n{\n  \"id\": \"user-uuid\",\n  \"name\": \"Jane Smith\",\n  \"email\": \"jane@example.com\",\n  \"bio\": \"Frontend Developer and Next.js enthusiast\",\n  \"avatar\": \"https://example.com/avatar.jpg\",\n  \"country\": \"United States\",\n  \"twitterAcc\": \"https://twitter.com/janesmith\",\n  \"githubAcc\": \"https://github.com/janesmith\",\n  \"linkedinAcc\": \"https://linkedin.com/in/janesmith\",\n  \"anotherAcc\": \"\",\n  \"createdAt\": \"2024-01-20T12:00:00Z\",\n  \"updatedAt\": \"2024-01-20T13:00:00Z\",\n  \"_count\": {\n    \"followers\": 150,\n    \"following\": 89,\n    \"blogs\": 25\n  },\n  \"blogs\": [],\n  \"followers\": [],\n  \"following\": []\n}\n\n#### Get User Followers\n\n- **URL**: `/api/users/:userId/followers`\n- **Method**: `GET`\n- **Auth**: Optional\n- **Response**:\n\n```json\n[\n  {\n    \"id\": \"user-uuid\",\n    \"name\": \"John Doe\",\n    \"email\": \"john@example.com\",\n    \"avatar\": \"https://example.com/avatar.jpg\"\n  },\n  {\n    \"id\": \"user-uuid2\",\n    \"name\": \"Jane Smith\",\n    \"email\": \"jane@example.com\",\n    \"avatar\": \"https://example.com/avatar2.jpg\"\n  }\n]\n````\n\n#### Get User Following\n\n- **URL**: `/api/users/:userId/following`\n- **Method**: `GET`\n- **Auth**: Optional\n- **Response**:\n\n```json\n[\n  {\n    \"id\": \"user-uuid\",\n    \"name\": \"John Doe\",\n    \"email\": \"john@example.com\",\n    \"avatar\": \"https://example.com/avatar.jpg\"\n  },\n  {\n    \"id\": \"user-uuid2\",\n    \"name\": \"Jane Smith\",\n    \"email\": \"jane@example.com\",\n    \"avatar\": \"https://example.com/avatar2.jpg\"\n  }\n]\n```\n\n#### Update User Profile\n\nUpdates the authenticated user's profile information.\n\n- **URL**: `/api/users/profile`\n- **Method**: `PUT`\n- **Auth**: Required\n- **Body**:\n  - Multipart form data that can include:\n\n```json\n{\n  \"name\": \"Jane Smith\",\n  \"bio\": \"Frontend Developer and Next.js enthusiast\",\n  \"country\": \"United States\",\n  \"twitterAcc\": \"https://twitter.com/janesmith\",\n  \"githubAcc\": \"https://github.com/janesmith\",\n  \"linkedinAcc\": \"https://linkedin.com/in/janesmith\",\n  \"anotherAcc\": \"\",\n  \"avatar\": \"[File Upload]\" // Optional profile image\n}\n```\n\n- **Response**:\n\n```json\n{\n  \"id\": \"user-uuid\",\n  \"name\": \"Jane Smith\",\n  \"email\": \"jane@example.com\",\n  \"bio\": \"Frontend Developer and Next.js enthusiast\",\n  \"avatar\": \"https://example.com/new-avatar.jpg\",\n  \"country\": \"United States\",\n  \"twitterAcc\": \"https://twitter.com/janesmith\",\n  \"githubAcc\": \"https://github.com/janesmith\",\n  \"linkedinAcc\": \"https://linkedin.com/in/janesmith\",\n  \"anotherAcc\": \"\",\n  \"createdAt\": \"2024-01-20T12:00:00Z\",\n  \"updatedAt\": \"2024-01-20T13:00:00Z\"\n}\n```\n\n#### Follow User\n\n- **URL**: `/api/users/:userId/follow`\n- **Method**: `POST`\n- **Auth**: Required\n\n#### Unfollow User\n\n- **URL**: `/api/users/:userId/follow`\n- **Method**: `DELETE`\n- **Auth**: Required\n\n#### following Status\n\n- **URL**: `/api/users/:userId/follow-status`\n- **Method**: `GET`\n- **Auth**: Required\n\n### Comments\n\n#### Create Comment\n\n- **URL**: `/api/blogs/:blogId/comments`\n- **Method**: `POST`\n- **Auth**: Required\n- **Body**:\n\n```json\n{\n  \"content\": \"Great article! Very informative.\",\n  \"parentId\": \"optional-parent-comment-id\" // Include for replies\n}\n```\n\n#### Get Comments\n\n- **URL**: `/api/blogs/:blogId/comments?page=1\u0026limit=10`\n- **Method**: `GET`\n- **Response**:\n\n```json\n{\n  \"comments\": [\n    {\n      \"id\": \"comment-uuid\",\n      \"content\": \"Great article! Very informative.\",\n      \"createdAt\": \"2024-01-20T12:00:00Z\",\n      \"authorId\": \"user-uuid\",\n      \"parentId\": null,\n      \"author\": {\n        \"id\": \"user-uuid\",\n        \"name\": \"John Doe\",\n        \"avatar\": \"https://example.com/avatar.jpg\"\n      },\n      \"replies\": [\n        {\n          \"id\": \"reply-uuid\",\n          \"content\": \"Thanks for your feedback!\",\n          \"createdAt\": \"2024-01-20T12:30:00Z\",\n          \"authorId\": \"author-uuid\",\n          \"parentId\": \"comment-uuid\",\n          \"author\": {\n            \"id\": \"author-uuid\",\n            \"name\": \"Jane Smith\",\n            \"avatar\": \"https://example.com/jane-avatar.jpg\"\n          }\n        }\n      ]\n    }\n  ],\n  \"pagination\": {\n    \"total\": 25,\n    \"pages\": 3,\n    \"current\": 1\n  }\n}\n```\n\n#### Update Comment\n\n- **URL**: `/api/comments/:id`\n- **Method**: `PATCH`\n- **Auth**: Required (only comment owner)\n- **Body**:\n\n```json\n{\n  \"content\": \"Updated comment content\"\n}\n```\n\n#### Delete Comment\n\n- **URL**: `/api/comments/:id`\n- **Method**: `DELETE`\n- **Auth**: Required (only comment owner)\n- **Response**: Status 204 (No Content)\n\n```\n\n\n#### Create/Delete like Blog\n- **URL**: `/api/blogs/:id/like`\n- **Method**: `POST`\n- **Auth**: Required\n- **Body**: json\n\n#### Create/Delete like Bookmark\n- **URL**: `/api/blogs/:id/bookmark`\n- **Method**: `POST`\n- **Auth**: Required\n- **Body**: json\n\n#### Get Interaction Status\n- **URL**: `/api/blogs/:id/interaction`\n- **Method**: `GET`\n- **Auth**: Required\n\n#### Get Blogs by Tags\n\n-   **URL**: `/api/blogs/tags/:tags?page=1\u0026limit=10`\n-   **Method**: `GET`\n\n#### Get Bookmarks\n- **URL**: `/api/blogs/bookmarks`\n- **Method**: `GET`\n- **Auth**: Required\n\n#### Get Blog by own\n- **URL**: `/api/blogs/bookmarks`\n- **Method**: `GET`\n- **Auth**: Required\n\n\n\n```\n\n### TODO\n\n## notification\n\nFlow Notifikasi Komentar/like blog (Tanpa Real-Time)\n1️⃣ User A menulis komentar di artikel blog/like blog milik User B melalui frontend.\n➡️ frontend kirim request ke Express.js (POST /api/comments).\n2️⃣ Express.js menyimpan komentar ke database.\n➡️ Setelah komentar disimpan, Express.js juga menyimpan notifikasi untuk User B (post owner).\n3️⃣ User B membuka halaman notifikasi di frontend.\n➡️ frontend melakukan GET request ke Express.js (GET /api/notifications?userId=userB) untuk mengambil notifikasi.\n4️⃣ User B membaca notifikasi (misalnya klik).\n➡️ frontend kirim PATCH request ke Express.js (PATCH /api/notifications/:id) untuk menandai notifikasi sudah dibaca.\n\n```\n\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkyyril%2Fbackend-blogs","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkyyril%2Fbackend-blogs","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkyyril%2Fbackend-blogs/lists"}