An exercise by The Odin Project( TOP ) to create a blog. I only designed the backend as that's my speciality :), then @muchubatactics did the frontend. The backend is a RESTful API built with Node.js, React( for the frontend ), Express.js, MongoDB, and JWT for authorization. It has 16+ endpoints for creating, reading, updating, and deleting users, posts, comments, and replies.
AUTHORS: muchubatactics & winterrdog
Make sure you have the following installed:
Postman/curl- for testing the APIDocker- for building the database(mongodb) and the applicationDocker Compose- for deploying the application. Use the new Go-based version,docker composeinstead of the old Python-based version,docker-composeotherwise you will get an error unless you use some workarounds.
-
To set up the environment variables, create a
.envfile in the root directory and copy the contents of.env.exampleinto it. Then fill in the missing values. -
To run the backend, run the following command:
cd ./src ./containerize.sh -
Stop the backend by running the following command:
docker compose down
-
In case you wanna run backend in production mode, run the following command:
cd ./src docker compose -f docker-compose.yml upRemember to set
MONGODB_URIto your production database URI somewhere in the cloud in the.envfile. -
The backend will be available at
http://localhost:3000.
-
To view logs, run the following command:
docker logs --tail=300 -f blog_backend
-
The container will also write logs to
./src/blog-app.logexternal to the container.
This backend has 3 features:
Authentication- for creating, deleting, updating and logging in usersPosts- for creating, reading, updating and deleting postsComments- for creating, reading, updating and deleting comments
It has 16+ endpoints i.e.:
POST /api/v1/users/sign-up- for registering a userPOST /api/v1/users/sign-in- for logging in a userPATCH /api/v1/users/update- for updating a user detailsDELETE /api/v1/users/delete- for deleting a userPATCH /api/v1/users/log-out- for logging out a user
POST /api/v1/posts/- for creating a postGET /api/v1/posts/user-posts- for getting a user's postsGET /api/v1/posts/liked-posts- for getting a user's liked postsGET /api/v1/posts/recently-viewed- for getting a user's 5 most recently viewed postsGET /api/v1/posts/- for getting all postsGET /api/v1/posts/:postId- for getting a postPATCH /api/v1/posts/:postId- for updating a postDELETE /api/v1/posts/:postId- for deleting a postPATCH /api/v1/posts/:postId/likes- for adding a likePATCH /api/v1/posts/:postId/dislikes- for adding a dislike
POST /api/v1/post-comments/:postId/comments/- for creating a commentGET /api/v1/post-comments/:postId/comments/- for getting all comments for a postGET /api/v1/post-comments/:postId/comments/:commentId- for getting a commentGET /api/v1/post-comments/user-comments- for getting a user's commentsGET /api/v1/post-comments/user-liked-comments- for getting a user's liked commentsPATCH /api/v1/post-comments/:postId/comments/:commentId- for updating a commentDELETE /api/v1/post-comments/:postId/comments/:commentId- for deleting a commentPATCH /api/v1/post-comments/:postId/comments/:commentId/likes- for adding a likeDELETE /api/v1/post-comments/:postId/comments/:commentId/likes- for removing a likePATCH /api/v1/post-comments/:postId/comments/:commentId/dislikes- for adding a dislikeDELETE /api/v1/post-comments/:postId/comments/:commentId/dislikes- for removing a dislikePOST /api/v1/post-comments/:postId/comments/:commentId/replies- for creating a replyPATCH /api/v1/post-comments/:postId/comments/:commentId/replies- for adding a replyDELETE /api/v1/post-comments/:postId/comments/:commentId/replies/:replyId- for deleting a reply
- The API uses
JWTfor authentication. The JWT is required in theAuthorizationheader for all requests except forsign-upandsign-inrequests and for user actions that only require reading data from the server i.eGETrequests.
- To test the API, you can use
Postmanorcurl. Below are some sample requests:
curl -X POST -H "Content-Type: application/json" -d '{
"name": "John Doe",
"pass": "password",
"role": "author"
}' http://localhost:3000/api/v1/users/sign-upThe server will respond with:
HTTP/1.1 201 Created
{
"message": "User created successfully",
"token": "eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7InN1YiI6IjY2MjY1NWMxYTJlMjkxYjg0NjM1ODlmZSIsInJvbGUiOiJhdXRob3IifSwiaWF0IjoxNzEzNzg4MzUzLCJleHAiOjE3MTM5NjExNTN9.8oc3DOAR6SqaWUBynMZlAvHJr202cTXbtiq80EVyO5RSXMJrdGJ-aWdZF_lfR1p4"
}curl -X POST -H "Content-Type: application/json" -d '{"name": "John Doe", "pass": "password"}' http://localhost:3000/api/v1/users/sign-inThe server will respond with:
HTTP/1.1 200 OK
{
"message": "User signed in successfully",
"token": "eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7InN1YiI6IjY2MjY1NWMxYTJlMjkxYjg0NjM1ODlmZSIsInJvbGUiOiJhdXRob3IifSwiaWF0IjoxNzEzNzg4MzUzLCJleHAiOjE3MTM5NjExNTN9.8oc3DOAR6SqaWUBynMZlAvHJr202cTXbtiq80EVyO5RSXMJrdGJ-aWdZF_lfR1p4"
}curl -X PATCH -H "Content-Type: application/json" -d '{"name": "Jane Doe", "role": "reader"}' http://localhost:3000/api/v1/users/updateThe server will respond with:
HTTP/1.1 200 OK
{
"message": "User updated",
"user": {
"name": "Jane Doe",
"role": "reader",
"id": "6623cfc033d53b054fe9b0c3",
"dateCreated": "2024-04-20T14:22:56.517Z",
"dateUpdated": "2024-04-22T12:29:46.406Z"
}
}curl -X DELETE -H "Authorization: Bearer eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7InN1YiI6IjY2MjY1NWMxYTJlMjkxYjg0NjM1ODlmZSIsInJvbGUiOiJhdXRob3IifSwiaWF0IjoxNzEzNzg4MzUzLCJleHAiOjE3MTM5NjExNTN9.8oc3DOAR6SqaWUBynMZlAvHJr202cTXbtiq80EVyO5RSXMJrdGJ-aWdZF_lfR1p4" http://localhost:3000/api/v1/users/deleteThe server will respond with:
HTTP/1.1 204 No Contentcurl -X PATCH -H "Authorization: Bearer eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7InN1YiI6IjY2MjY1NWMxYTJlMjkxYjg0NjM1ODlmZSIsInJvbGUiOiJhdXRob3IifSwiaWF0IjoxNzEzNzg4MzUzLCJleHAiOjE3MTM5NjExNTN9.8oc3DOAR6SqaWUBynMZlAvHJr202cTXbtiq80EVyO5RSXMJrdGJ-aWdZF_lfR1p4" http://localhost:3000/api/v1/users/log-outThe server will respond with:
HTTP/1.1 204 No Content
Only registered users can create posts. The JWT is required in the Authorization header.
curl -X POST -H "Content-Type: application/json" -H "
Authorization: Bearer eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7InN1YiI6IjY2MjY1NWMxYTJlMjkxYjg0NjM1ODlmZSIsInJvbGUiOiJhdXRob3IifSwiaWF0IjoxNzEzNzg4MzUzLCJleHAiOjE3MTM5NjExNTN9.8oc3DOAR6SqaWUBynMZlAvHJr202cTXbtiq80EVyO5RSXMJrdGJ-aWdZF_lfR1p4" -d '{
"title": "My first post",
"body": "This is my first post. I hope you like it."
}' http://localhost:3000/api/v1/posts/The server will respond with:
HTTP/1.1 201 Created
{
"message": "Post created successfully",
"post": {
"author": "John Doe",
"title": "My first post",
"body": "This is my first post. I hope you like it.",
"hidden": false,
"id": "662659aaa2e291b846358a06",
"dateCreated": "2024-04-22T12:35:54.030Z",
"dateUpdated": "2024-04-22T12:35:54.030Z"
}
}Make sure the JWT is in the Authorization header since we rely on it to get the user's posts.
curl -X GET
-H "Authorization: Bearer eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7InN1YiI6IjY2MjY1NWMxYTJlMjkxYjg0NjM1ODlmZSIsInJvbGUiOiJhdXRob3IifSwiaWF0IjoxNzEzNzg4MzUzLCJleHAiOjE3MTM5NjExNTN9.8oc3DOAR6SqaWUBynMZlAvHJr202cTXbtiq80EVyO5RSXMJrdGJ-aWdZF_lfR1p4"
http://localhost:3000/api/v1/posts/user-postsThe server will respond with:
HTTP/1.1 200 OK
{
"message": "User's posts retrieved successfully",
"posts": [
{
"author": "John Doe",
"title": "My first post",
"body": "This is my first post. I hope you like it.",
"hidden": false,
"id": "662659aaa2e291b846358a06",
"dateCreated": "2024-04-22T12:35:54.030Z",
"dateUpdated": "2024-04-22T12:35:54.030Z"
}
]
}Make sure the JWT is in the Authorization header since we rely on it to get the user's liked posts.
curl -X GET
-H "Authorization: Bearer eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7InN1YiI6IjY2MjY1NWMxYTJlMjkxYjg0NjM1ODlmZSIsInJvbGUiOiJhdXRob3IifSwiaWF0IjoxNzEzNzg4MzUzLCJleHAiOjE3MTM5NjExNTN9.8oc3DOAR6SqaWUBynMZlAvHJr202cTXbtiq80EVyO5RSXMJrdGJ-aWdZF_lfR1p4"
http://localhost:3000/api/v1/posts/liked-postsThe server will respond with:
HTTP/1.1 200 OK
{
"message": "User's liked posts retrieved successfully",
"posts": [
{
"author": "John Doe",
"title": "My first post",
"body": "This is my first post. I hope you like it.",
"hidden": false,
"id": "662659aaa2e291b846358a06",
"dateCreated": "2024-04-22T12:35:54.030Z",
"dateUpdated": "2024-04-22T12:35:54.030Z"
}
]
}Make sure the JWT is in the Authorization header since we rely on it to get the user's recently viewed posts.
curl -X GET
-H "Authorization: Bearer eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7InN1YiI6IjY2MjY1NWMxYTJlMjkxYjg0NjM1ODlmZSIsInJvbGUiOiJhdXRob3IifSwiaWF0IjoxNzEzNzg4MzUzLCJleHAiOjE3MTM5NjExNTN9.8oc3DOAR6SqaWUBynMZlAvHJr202cTXbtiq80EVyO5RSXMJrdGJ-aWdZF_lfR1p4"
http://localhost:3000/api/v1/posts/recently-viewedThe server will respond with:
HTTP/1.1 200 OK
{
"message": "User's recently viewed posts retrieved successfully",
"posts": [
{
"author": "John Doe",
"title": "My first post",
"body": "This is my first post. I hope you like it.",
"hidden": false,
"id": "662659aaa2e291b846358a06",
"dateCreated": "2024-04-22T12:35:54.030Z",
"dateUpdated": "2024-04-22T12:35:54.030Z"
}
]
}It will return a maximum of 5 posts. If the user has viewed less than 5 posts, it will return all the posts the user has viewed.
For the case when there's no post viewed by the user, the server will respond with a 404 status code and the following message:
HTTP/1.1 404 Not Found
{
message: "no recently viewed posts found for user",
}curl -X GET http://localhost:3000/api/v1/posts/The server will respond with:
HTTP/1.1 200 OK
{
"message": "Posts retrieved successfully",
"posts": [
{
"author": "John Doe",
"title": "My first post",
"body": "This is my first post. I hope you like it.",
"hidden": false,
"id": "662659aaa2e291b846358a06",
"dateCreated": "2024-04-22T12:35:54.030Z",
"dateUpdated": "2024-04-22T12:35:54.030Z"
},
{
"author": "Jane Doe",
"title": "My second post",
"body": "This is my second post. I hope you like it.",
"hidden": false,
"id": "662659aaa2e291b846358a07",
"dateCreated": "2024-04-22T12:35:54.030Z",
"dateUpdated": "2024-04-22T12:35:54.030Z"
}
]
}curl -X GET http://localhost:3000/api/v1/posts/662659aaa2e291b846358a06The server will respond with:
HTTP/1.1 200 OK
{
"message": "Post retrieved successfully",
"post": {
"author": "John Doe",
"title": "My first post",
"body": "This is my first post. I hope you like it.",
"hidden": false,
"id": "662659aaa2e291b846358a06",
"dateCreated": "2024-04-22T12:35:54.030Z",
"dateUpdated": "2024-04-22T12:35:54.030Z"
}
}curl -X PATCH -H "Content-Type: application/json" -H "Authorization: Bearer eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7InN1YiI6IjY2MjY1NWMxYTJlMjkxYjg0NjM1ODlmZSIsInJvbGUiOiJhdXRob3IifSwiaWF0IjoxNzEzNzg4MzUzLCJleHAiOjE3MTM5NjExNTN9.8oc3DOAR6SqaWUBynMZlAvHJr202cTXbtiq80EVyO5RSXMJrdGJ-aWdZF_lfR1p4" -d '{
"title": "My first post",
"body": "This is my first post. I hope you like it. I have updated it."
}' http://localhost:3000/api/v1/posts/662659aaa2e291b846358a06The server will respond with:
HTTP/1.1 200 OK
{
"message": "Post updated",
"post": {
"author": "John Doe",
"title": "My first post",
"body": "This is my first post. I hope you like it. I have updated it.",
"hidden": false,
"id": "662659aaa2e291b846358a06",
"dateCreated": "2024-04-22T12:35:54.030Z",
"dateUpdated": "2024-04-22T12:35:54.030Z"
}
}curl -X DELETE -H "Authorization: Bearer eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7InN1YiI6IjY2MjY1NWMxYTJlMjkxYjg0NjM1ODlmZSIsInJvbGUiOiJhdXRob3IifSwiaWF0IjoxNzEzNzg4MzUzLCJleHAiOjE3MTM5NjExNTN9.8oc3DOAR6SqaWUBynMZlAvHJr202cTXbtiq80EVyO5RSXMJrdGJ-aWdZF_lfR1p4" http://localhost:3000/api/v1/posts/662659aaa2e291b846358a06The server will respond with:
HTTP/1.1 204 No ContentOnly registered users can create comments. The JWT is required in the Authorization header.
curl -X POST -H "Content-Type: application/json" -H "Authorization: Bearer eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7InN1YiI6IjY2MjY1NWMxYTJlMjkxYjg0NjM1ODlmZSIsInJvbGUiOiJhdXRob3IifSwiaWF0IjoxNzEzNzg4MzUzLCJleHAiOjE3MTM5NjExNTN9.8oc3DOAR6SqaWUBynMZlAvHJr202cTXbtiq80EVyO5RSXMJrdGJ-aWdZF_lfR1p4" -d '{
"body": "This is a great post. I love it."
}' http://localhost:3000/api/v1/post-comments/662659aaa2e291b846358a07/comments/The server will respond with:
HTTP/1.1 201 Created
{
"message": "comment created successfully",
"comment": {
"body": "how does it work?",
"tldr": "work",
"user": "kaboom",
"post": "6623d00e33d53b054fe9b0c7",
"id": "66265bcda2e291b846358a17",
"dateCreated": "2024-04-22T12:45:01.875Z",
"dateUpdated": "2024-04-22T12:45:01.875Z"
}
}curl -X GET http://localhost:3000/api/v1/post-comments/662659aaa2e291b846358a07/comments/The server will respond with:
HTTP/1.1 200 OK
{
"message": "post comments fetched successfully",
"comments": [
{
"body": "This is a great post. I love it.",
"tldr": "great post",
"user": "John Doe",
"post": "662659aaa2e291b846358a07",
"id": "66265bcda2e291b846358a17",
"dateCreated": "2024-04-22T12:45:01.875Z",
"dateUpdated": "2024-04-22T12:45:01.875Z"
}
]
}Make sure the JWT is in the Authorization header since we rely on it to get the user's comments.
curl -X GET
-H "Authorization: Bearer eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7InN1YiI6IjY2MjY1NWMxYTJlMjkxYjg0NjM1ODlmZSIsInJvbGUiOiJhdXRob3IifSwiaWF0IjoxNzEzNzg4MzUzLCJleHAiOjE3MTM5NjExNTN9.8oc3DOAR6SqaWUBynMZlAvHJr202cTXbtiq80EVyO5RSXMJrdGJ-aWdZF_lfR1p4"
http://localhost:3000/api/v1/post-comments/user-commentsThe server will respond with:
HTTP/1.1 200 OK
{
"message": "user comments fetched successfully",
"comments": [
{
"body": "This is a great post. I love it.",
"tldr": "great post",
"user": "John Doe",
"post": "662659aaa2e291b846358a07",
"id": "66265bcda2e291b846358a17",
"dateCreated": "2024-04-22T12:45:01.875Z",
"dateUpdated": "2024-04-22T12:45:01.875Z"
}
]
}Make sure the JWT is in the Authorization header since we rely on it to get the user's liked comments.
curl -X GET
-H "Authorization: Bearer eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7InN1YiI6IjY2MjY1NWMxYTJlMjkxYjg0NjM1ODlmZSIsInJvbGUiOiJhdXRob3IifSwiaWF0IjoxNzEzNzg4MzUzLCJleHAiOjE3MTM5NjExNTN9.8oc3DOAR6SqaWUBynMZlAvHJr202cTXbtiq80EVyO5RSXMJrdGJ-aWdZF_lfR1p4"
http://localhost:3000/api/v1/post-comments/user-liked-commentsOn success, the server will respond with:
HTTP/1.1 200 OK
{
"message": "user liked comments fetched successfully",
"comments": [
{
"body": "This is a great post. I love it.",
"tldr": "great post",
"user": "John Doe",
"post": "662659aaa2e291b846358a07",
"id": "66265bcda2e291b846358a17",
"dateCreated": "2024-04-22T12:45:01.875Z",
"dateUpdated": "2024-04-22T12:45:01.875Z"
}
]
}In case no liked comments are found, the server will respond with:
HTTP/1.1 404 Not Found
{
"message": "no comments were liked by the user",
}curl -X GET http://localhost:3000/api/v1/post-comments/662659aaa2e291b846358a07/comments/66265bcda2e291b846358a17The server will respond with:
HTTP/1.1 200 OK
{
"message": "comment fetched successfully",
"comment": {
"body": "This is a great post. I love it.",
"tldr": "great post",
"user": "John Doe",
"post": "662659aaa2e291b846358a07",
"id": "66265bcda2e291b846358a17",
"dateCreated": "2024-04-22T12:45:01.875Z",
"dateUpdated": "2024-04-22T12:45:01.875Z"
}
}curl -X PATCH -H "Content-Type: application/json" -H "Authorization: Bearer eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7InN1YiI6IjY2MjY1NWMxYTJlMjkxYjg0NjM1ODlmZSIsInJvbGUiOiJhdXRob3IifSwiaWF0IjoxNzEzNzg4MzUzLCJleHAiOjE3MTM5NjExNTN9.8oc3DOAR6SqaWUBynMZlAvHJr202cTXbtiq80EVyO5RSXMJrdGJ-aWdZF_lfR1p4" -d '{
"body": "This is a great post. I love it. I have updated it."
}' http://localhost:3000/api/v1/post-comments/662659aaa2e291b846358a07/comments/66265bcda2e291b846358a17The server will respond with:
HTTP/1.1 200 OK
{
"message": "post comment updated successfully",
"comment": {
"body": "This is a great post. I love it. I have updated it.",
"tldr": "great post",
"user": "John Doe",
"post": "662659aaa2e291b846358a07",
"id": "66265bcda2e291b846358a17",
"dateCreated": "2024-04-22T12:45:01.875Z",
"dateUpdated": "2024-04-22T12:45:01.875Z"
}
}curl -X DELETE -H "Authorization: Bearer eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7InN1YiI6IjY2MjY1NWMxYTJlMjkxYjg0NjM1ODlmZSIsInJvbGUiOiJhdXRob3IifSwiaWF0IjoxNzEzNzg4MzUzLCJleHAiOjE3MTM5NjExNTN9.8oc3DOAR6SqaWUBynMZlAvHJr202cTXbtiq80EVyO5RSXMJrdGJ-aWdZF_lfR1p4" http://localhost:3000/api/v1/post-comments/662659aaa2e291b846358a07/comments/66265bcda2e291b846358a17The server will respond with:
HTTP/1.1 204 No Contentcurl -X PATCH
-H "Authorization
: Bearer eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7InN1YiI6IjY2MjY1NWMxYTJlMjkxYjg0NjM1ODlmZSIsInJvbGUiOiJhdXRob3IifSwiaWF0IjoxNzEzNzg4MzUzLCJleHAiOjE3MTM5NjExNTN9.8oc3DOAR6SqaWUBynMZlAvHJr202cTXbtiq80EVyO5RSXMJrdGJ-aWdZF_lfR1p4" http://localhost:3000/api/v1/post-comments/662659aaa2e291b846358a07/comments/66265bcda2e291b846358a17/likesThe server will respond with:
HTTP/1.1 200 OK
{
"message": "like added successfully",
"comment": {
"body": "This is a great post. I love it. I have updated it.",
"tldr": "great post",
"user": "John Doe",
"post": "662659aaa2e291b846358a07",
"id": "66265bcda2e291b846358a17",
"dateCreated": "2024-04-22T12:45:01.875Z",
"dateUpdated": "2024-04-22T12:45:01.875Z",
"likes": 1,
"dislikes": 0
}
}curl -X DELETE
-H "Authorization
: Bearer eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7InN1YiI6IjY2MjY1NWMxYTJlMjkxYjg0NjM1ODlmZSIsInJvbGUiOiJhdXRob3IifSwiaWF0IjoxNzEzNzg4MzUzLCJleHAiOjE3MTM5NjExNTN9.8oc3DOAR6SqaWUBynMZlAvHJr202cTXbtiq80EVyO5RSXMJrdGJ-aWdZF_lfR1p4" http://localhost:3000/api/v1/post-comments/662659aaa2e291b846358a07/comments/66265bcda2e291b846358a17/likesThe server will respond with:
HTTP/1.1 200 OK
{
"message": "like removed successfully",
"comment": {
"body": "This is a great post. I love it. I have updated it.",
"tldr": "great post",
"user": "John Doe",
"post": "662659aaa2e291b846358a07",
"id": "66265bcda2e291b846358a17",
"dateCreated": "2024-04-22T12:45:01.875Z",
"dateUpdated": "2024-04-22T12:45:01.875Z",
"likes": 0,
"dislikes": 0
}
}They follow the same pattern as likes. The only difference is that they are for disliking a comment. The endpoints change from likes to dislikes but the method remains the same.
They follow the same pattern as comments. The only difference is that they are nested under comments.
LICENSE: Unlicense