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 REST
ful 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 compose
instead of the old Python-based version,docker-compose
otherwise you will get an error unless you use some workarounds.
-
To set up the environment variables, create a
.env
file in the root directory and copy the contents of.env.example
into 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 up
Remember to set
MONGODB_URI
to your production database URI somewhere in the cloud in the.env
file. -
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.log
external 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
JWT
for authentication. The JWT is required in theAuthorization
header for all requests except forsign-up
andsign-in
requests and for user actions that only require reading data from the server i.eGET
requests.
- To test the API, you can use
Postman
orcurl
. 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-up
The 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-in
The 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/update
The 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/delete
The server will respond with:
HTTP/1.1 204 No Content
curl -X PATCH -H "Authorization: Bearer eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7InN1YiI6IjY2MjY1NWMxYTJlMjkxYjg0NjM1ODlmZSIsInJvbGUiOiJhdXRob3IifSwiaWF0IjoxNzEzNzg4MzUzLCJleHAiOjE3MTM5NjExNTN9.8oc3DOAR6SqaWUBynMZlAvHJr202cTXbtiq80EVyO5RSXMJrdGJ-aWdZF_lfR1p4" http://localhost:3000/api/v1/users/log-out
The 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-posts
The 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-posts
The 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-viewed
The 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/662659aaa2e291b846358a06
The 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/662659aaa2e291b846358a06
The 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/662659aaa2e291b846358a06
The server will respond with:
HTTP/1.1 204 No Content
Only 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-comments
The 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-comments
On 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/66265bcda2e291b846358a17
The 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/66265bcda2e291b846358a17
The 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/66265bcda2e291b846358a17
The server will respond with:
HTTP/1.1 204 No Content
curl -X PATCH
-H "Authorization
: Bearer eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7InN1YiI6IjY2MjY1NWMxYTJlMjkxYjg0NjM1ODlmZSIsInJvbGUiOiJhdXRob3IifSwiaWF0IjoxNzEzNzg4MzUzLCJleHAiOjE3MTM5NjExNTN9.8oc3DOAR6SqaWUBynMZlAvHJr202cTXbtiq80EVyO5RSXMJrdGJ-aWdZF_lfR1p4" http://localhost:3000/api/v1/post-comments/662659aaa2e291b846358a07/comments/66265bcda2e291b846358a17/likes
The 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/likes
The 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