├── .gitignore ├── loadEnv.js ├── assets └── images │ └── preview.png ├── db └── conn.js ├── models ├── recipes.js ├── reviews.js └── users.js ├── package.json ├── index.js ├── routes ├── reviews.js ├── recipes.js └── users.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env -------------------------------------------------------------------------------- /loadEnv.js: -------------------------------------------------------------------------------- 1 | import dotenv from "dotenv"; 2 | dotenv.config(); 3 | -------------------------------------------------------------------------------- /assets/images/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/copilpatricia/capstone_project_backend/HEAD/assets/images/preview.png -------------------------------------------------------------------------------- /db/conn.js: -------------------------------------------------------------------------------- 1 | // create a connection 2 | 3 | import mongoose from 'mongoose'; 4 | 5 | export async function conn() { 6 | try { 7 | await mongoose.connect(process.env.ATLAS_URI); 8 | console.log('Connected to MongoDB') 9 | } catch(error) { 10 | console.log(error.message); 11 | } 12 | } -------------------------------------------------------------------------------- /models/recipes.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const recipesSchema = new mongoose.Schema({ 4 | title: { 5 | type: String, 6 | required: true, 7 | }, 8 | 9 | image: { 10 | type: String, 11 | required: true, 12 | }, 13 | 14 | ingredients: { 15 | type: Array, 16 | default: [], 17 | }, 18 | 19 | instructions: { 20 | type: Array, 21 | default: [] 22 | } 23 | }); 24 | 25 | export default mongoose.model("Recipes", recipesSchema); 26 | -------------------------------------------------------------------------------- /models/reviews.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const reviewsSchema = new mongoose.Schema({ 4 | // user is is associated with a review - allows the user to create a review 5 | user_id: { 6 | type: mongoose.Schema.Types.ObjectId, 7 | ref: "Users", 8 | required: true 9 | }, 10 | username: { 11 | type: String, 12 | 13 | }, 14 | review: { 15 | type: String, 16 | 17 | } 18 | }, {timestamps: true}); 19 | 20 | export default mongoose.model("Reviews", reviewsSchema) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "start": "node index.js", 10 | "dev": "nodemon index.js" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "dependencies": { 16 | "bcrypt": "^5.1.1", 17 | "cors": "^2.8.5", 18 | "dotenv": "^16.4.1", 19 | "express": "^4.18.2", 20 | "mongoose": "^8.1.1", 21 | "morgan": "^1.10.0" 22 | }, 23 | "devDependencies": { 24 | "nodemon": "^3.0.3" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /models/users.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import bcrypt from 'bcrypt'; 3 | 4 | const SALT_ROUNDS = 8; 5 | 6 | const usersSchema = new mongoose.Schema({ 7 | username: { 8 | type: String, 9 | required: true, 10 | minLength: 3, 11 | maxLength: 20 12 | }, 13 | 14 | email: { 15 | type: String, 16 | required: true, 17 | unique: true 18 | }, 19 | 20 | password: { 21 | type: String, 22 | required: true, 23 | minLength: 6, 24 | maxLength: 20 25 | } 26 | }, { 27 | timestamps: true 28 | }) 29 | 30 | usersSchema.index({email: 1}) 31 | 32 | // hide the password using bcrypt 33 | usersSchema.pre('save', async function(next){ 34 | // if the password has not change continue 35 | if(!this.isModified("password")) return next(); 36 | 37 | this.password = await bcrypt.hash(this.password, SALT_ROUNDS); 38 | return next() 39 | }) 40 | 41 | export default mongoose.model("Users", usersSchema) -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import './loadEnv.js'; 2 | import {conn} from './db/conn.js'; 3 | conn(); 4 | import express from 'express'; 5 | import morgan from 'morgan'; 6 | import cors from 'cors'; 7 | 8 | import recipesRouter from './routes/recipes.js' 9 | import usersRouter from './routes/users.js' 10 | import reviewsRouter from './routes/reviews.js' 11 | 12 | const app = express() 13 | const PORT = process.env.PORT || 4000; 14 | 15 | // middlewares 16 | app.use(cors()); // allows frontend to connect to backend 17 | app.use(morgan("dev")); //logger - info about the request 18 | app.use(express.json()); // for data in req.body 19 | app.use(express.urlencoded({extended: true})) // allow data in url string 20 | 21 | //routes 22 | app.use('/api/recipes', recipesRouter); 23 | app.use('/api/users', usersRouter) 24 | app.use('/api/reviews', reviewsRouter) 25 | 26 | 27 | app.get('/', (req, res) => { 28 | res.send('Welcome to the API...') 29 | }) 30 | 31 | app.listen(PORT, () => { 32 | console.log(`Server running on ${PORT}`); 33 | }) -------------------------------------------------------------------------------- /routes/reviews.js: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import Reviews from "../models/reviews.js"; 3 | 4 | const router = new Router(); 5 | 6 | // GET route - returns all reviews 7 | 8 | router.get("/", async (req, res) => { 9 | try { 10 | const reviews = await Reviews.find({}); 11 | if (!reviews) return res.status(404).json({ msg: "Reviews not found" }); 12 | else res.json(reviews); 13 | } catch (error) { 14 | console.log(error); 15 | } 16 | }); 17 | 18 | // GET /:id router - return a single review 19 | 20 | router.get("/:id", async (req, res) => { 21 | try { 22 | const { id } = req.params; 23 | const review = await Reviews.findById(id); 24 | if (!review) return res.status(404).json({ msg: "Review not found!" }); 25 | } catch (error) { 26 | console.log(error); 27 | } 28 | }); 29 | 30 | // POST router - create a review 31 | 32 | router.post("/", async (req, res) => { 33 | console.log(req.body); 34 | try { 35 | const reviews = await Reviews.create(req.body); 36 | console.log(reviews); 37 | res.status(201).json(reviews); 38 | } catch (error) { 39 | console.log(error); 40 | } 41 | }); 42 | 43 | // PUT route - to update a review 44 | 45 | router.put("/:id", async (req, res) => { 46 | try { 47 | const { id } = req.params; 48 | const { body } = req; 49 | 50 | const updateReview = await Reviews.findByIdAndUpdate(id, body, { 51 | new: true, 52 | }); 53 | res.json(updateReview); 54 | } catch (error) { 55 | console.log(error); 56 | } 57 | }); 58 | 59 | // DELETE route - to delete a review 60 | 61 | router.delete("/:id", async (req, res) => { 62 | try { 63 | const { id } = req.params; 64 | const deleteReview = await Reviews.findByIdAndDelete(id); 65 | res.json({ msg: "Review deleted", deleteReview }); 66 | } catch (error) { 67 | console.log(error); 68 | } 69 | }); 70 | 71 | export default router; 72 | -------------------------------------------------------------------------------- /routes/recipes.js: -------------------------------------------------------------------------------- 1 | import {Router} from 'express'; 2 | import Recipes from '../models/recipes.js' 3 | 4 | const router = new Router(); 5 | // GET route - returns all recipes 6 | 7 | router.get('/', async(req, res) => { 8 | try { 9 | const recipes = await Recipes.find({}); 10 | res.status(200).json(recipes) 11 | } catch (error) { 12 | console.log(error) 13 | 14 | } 15 | }) 16 | 17 | // GET /:id route - return a single recipe selected by the id 18 | 19 | router.get('/:id', async(req, res) => { 20 | try { 21 | const {id} = req.params; 22 | console.log("ID:", id); 23 | const recipes = await Recipes.findById(id); 24 | res.status(200).json(recipes) 25 | } catch (error) { 26 | console.log(error); 27 | res.json({msg: "Recipe not found!"}) 28 | 29 | } 30 | }); 31 | 32 | // POST route - to create new recipe 33 | 34 | router.post('/', async(req, res) => { 35 | try { 36 | const recipes = await Recipes.create(req.body); 37 | res.status(203).json(recipes); 38 | } catch (error) { 39 | console.log(error); 40 | } 41 | }); 42 | 43 | //PUT route - to update a recipe 44 | 45 | router.put('/:id', async(req, res) => { 46 | try { 47 | const {id} = req.params; 48 | const {body} = req; 49 | const updateRecipe = await Recipes.findByIdAndUpdate(id, body, {new: true}); 50 | res.json(updateRecipe) 51 | } catch (error) { 52 | console.log(error); 53 | res.json({msg: "Recipe not found!"}) 54 | 55 | } 56 | }) 57 | 58 | // DELETE route - to delete a recipe 59 | 60 | router.delete('/:id', async(req, res) => { 61 | try { 62 | const {id} = req.params; 63 | const deleteRecipe = await Recipes.findByIdAndDelete(id); 64 | res.json({msg: "Recipe deleted", deleteRecipe}) 65 | } catch (error) { 66 | console.log(error) 67 | } 68 | }) 69 | 70 | export default router; -------------------------------------------------------------------------------- /routes/users.js: -------------------------------------------------------------------------------- 1 | import {Router} from 'express'; 2 | import Users from '../models/users.js'; 3 | import Reviews from '../models/reviews.js'; 4 | import bcrypt from 'bcrypt'; 5 | 6 | const router = new Router(); 7 | 8 | // GET route - returns all users 9 | 10 | router.get('/', async(req, res) => { 11 | try { 12 | const users = await Users.find({}).populate({path: "user_id"}); 13 | if(!users) return res.status(404).json({msg: "User not found!"}); 14 | else res.json(users); 15 | } catch (error) { 16 | console.log(error) 17 | } 18 | }) 19 | 20 | // GET/:id router - returns a single user 21 | 22 | router.get('/:id', async(req, res) => { 23 | try { 24 | const {id} = req.params 25 | const user = await Users.findById(id) 26 | if(!user) return res.status(404).json({msg: "User not found!"}); 27 | else res.json(user) 28 | } catch (error) { 29 | console.log(error); 30 | } 31 | }) 32 | 33 | // POST route - creates a new user 34 | 35 | router.post('/', async(req, res) => { 36 | try { 37 | const users = await Users.create(req.body); 38 | const review = await Reviews.create({ 39 | user_id: users._id, 40 | username: users.username, 41 | review: req.body.review 42 | 43 | }); 44 | 45 | res.status(203).json({users, review}) 46 | } catch (error) { 47 | console.log(error) 48 | } 49 | }) 50 | 51 | // PUT route - to update an user 52 | 53 | router.put('/:id', async(req, res) => { 54 | try { 55 | const {id} = req.params; 56 | const {body} = req; 57 | 58 | if (body.password) { 59 | delete body.password; 60 | } 61 | 62 | const updatedUser = await Users.findByIdAndUpdate(id, body, {new: true}); 63 | res.json(updatedUser) 64 | } catch (error) { 65 | console.log(error) 66 | } 67 | }) 68 | 69 | // DELETE route - to delete an user 70 | 71 | router.delete('/:id', async(req, res) => { 72 | try { 73 | const {id} = req.params; 74 | const deletedUser = await Users.findByIdAndDelete(id); 75 | res.json({msg: "User deleted", deletedUser}) 76 | } catch (error) { 77 | console.log(error) 78 | } 79 | }) 80 | 81 | //POST ROUTE - for the signup form 82 | 83 | router.post('/signup', async(req, res) => { 84 | try { 85 | console.log(req.body) 86 | const {email, password} = req.body; 87 | const user = await Users.findOne({email}); 88 | if(user) { 89 | return res.status(401).json({msg: "User already exist!"}) 90 | } 91 | 92 | const createUser = await Users.create(req.body) 93 | res.status(203).json(createUser) 94 | } catch (error) { 95 | console.log(error) 96 | } 97 | }) 98 | 99 | // POST ROUTE - for the singin form 100 | 101 | router.post('/signin', async(req, res) => { 102 | try { 103 | console.log(req.body) 104 | const {email, password} = req.body 105 | const user = await Users.findOne({email}); 106 | 107 | if(!user) { 108 | return res.status(401).json({msg: "Invalid data!"}) 109 | } 110 | 111 | const passwordMatched = await bcrypt.compare(password, user.password); 112 | 113 | if (!passwordMatched) { 114 | return res.status(401).json({msg: "Invalid password"}) 115 | } 116 | res.json(user) 117 | } catch (error) { 118 | console.log(error) 119 | } 120 | }) 121 | 122 | export default router -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Blog Application - Capstone Project 2 | 3 | ![This is a preview of the Main Page](./assets/images/preview.png) 4 | 5 | ## Description 6 | 7 | Taste the joy is a full-stack project that combines the power of MongoDB, Express, React and Node.js to deliver a user-friendly experience. 8 | 9 | This SPA ensures a smooth user experience, making it easy to explore the variety of recipes and user reviews. 10 | 11 | Users can not only browse recipes but also contribute to the community by adding their own reviews, and sharing insights with the other users. 12 | 13 | Behind the scenes, I have used MongoDB and Mongoose to create an efficient way to store and manipulate the data, making the entire application responsive. 14 | 15 | The application is a full CRUD system for recipes, reviews and user users profile. 16 | 17 | I used the React library to render the Navbar component and Footer component, and also to create different views for the application. To navigate without refreshing pages I utilized React Router framework. To manage the state of the application I used the useState hook. 18 | 19 | In order to provide an efficient way to share the user-related state across various components in application I used the useContext hook. 20 | 21 | ## Getting Started 22 | 23 | Would you like to see the content of the page? Please use the following credentials to log in:( email: user1, password: password1) or create an account using the Sign Up button. Click this link to get access to the page: https://coruscating-hamster-e9dfd0.netlify.app/ . 24 | 25 | # Backend Configuration 26 | 27 | ## API Entities / Collection 28 | 29 | 1. Users 30 | 2. Recipes 31 | 3. Reviews 32 | 33 | ## Routes 34 | 35 | Path: / Method: GET Description: Returns welcome message: "Welcome tot the API"! 36 | 37 | ## API Routes - CRUD 38 | 39 | - User Routes 40 | 41 | Path: /api/users Method: GET Description: Returns all users 42 | 43 | Path: /api/users/:id Method: GET Description: Returns a single users selected by id 44 | 45 | Path: /api/users Method: POST Body: {username: String, password: String} Description: Creates a new User 46 | 47 | Path: /api/users/:id Method: PUT Description: Update an existing user selected by the id 48 | 49 | Path: /api/users/:id Method: DELETE Description: Delete a user selected by the id 50 | 51 | Path: /api/users/signup Method: POST Description: Allow the user to sign up and access the page 52 | 53 | Path: /api/users/signin Method: POST Description: Allow the user to sign in and access the page 54 | 55 | - Recipes Routes 56 | 57 | Path: /api/recipes Method: GET Description: Returns all recipes 58 | 59 | Path: /api/recipes/:id Method: GET Description: Returns a single recipe selected by id 60 | 61 | Path: /api/recipes Method: POST Body: {title: String, image: String, ingredients: Array, instructions: Array} Description: Creates a new recipe 62 | 63 | Path: /api/recipes/:id Method: PUT Description: Update an existing recipe selected by the id 64 | 65 | Path: /api/recipes/:id Method: DELETE Description: Delete a recipe selected by the id 66 | 67 | - Reviews Routes 68 | 69 | Path: /api/reviews Method: GET Description: Returns all reviews 70 | 71 | Path: /api/reviews/:id Method: GET Description: Returns a single review selected by id 72 | 73 | Path: /api/reviews Method: POST Body: {user_id: ref: "Users", username: String, review: String} Description: Creates a new review 74 | 75 | Path: /api/review/:id Method: PUT Description: Update an existing review selected by the id 76 | 77 | Path: /api/review/:id Method: DELETE Description: Delete a review selected by the id 78 | 79 | ## Future updates 80 | 81 | The blog application is going to have more features in the future. 82 | 83 | 1. I am going to implement a search bar for the user to search the recipes easily. 84 | 2. I am planning to create a section where users can add their own recipes. 85 | 86 | ## Resources 87 | 88 | - [Trello website](https://trello.com/b/V2Mymh3I/capstone-project-blog-app) 89 | 90 | - [Draw.io](https://app.diagrams.net/) 91 | 92 | - [Mongoose Documentation](https://mongoosejs.com/docs/) 93 | 94 | - [Inspirational food blog](https://pinchofyum.com/about) 95 | 96 | - [Game of Thrones API](https://api.gameofthronesquotes.xyz/v1/random) 97 | 98 | - Stackoverflow : 99 | 100 | 1. https://stackoverflow.com/questions/37669391/how-to-get-rid-of-underline-for-link-component-of-react-router 101 | 102 | 2. https://stackoverflow.com/questions/18001478/referencing-another-schema-in-mongoose 103 | 104 | ## Other links 105 | 106 | Link to the frontend repo: https://github.com/copilpatricia/capstone_project_frontend 107 | 108 | Successfully deployed the backend here: https://blog-app-backend-nrpv.onrender.com 109 | 110 | Successfully deployed the frontend here: https://app.netlify.com/sites/coruscating-hamster-e9dfd0/overview 111 | --------------------------------------------------------------------------------