Horje
Recipe Manager with MERN Stack

The Recipe Manager is a popular MERN Stack-based project used for managing various recipes and creating, editing, or deleting recipes. This article will cover the steps involved in creating a MERN Stack-based project called “Recipe Manager” that allows users to manage multiple recipes.

Output Preview:

Screenshot-(79)-compressed

Recipe Manager

Prerequisites

Approach

  • Start by creating an index file in the backend folder & install node modules in it.
  • Connect the backend to a mongodb server by creating a configuration in it & connect it to the driver.
  • Create models & routes for the recipes, as in, where should the page be directed to when the specific url is entered. And in the models, create specific schemas for both the user & recipes page to store in data and the kind of data.
  • In the client side, start by creating several routes for different pages like saved recipes, create recipes, view recipes & authentication page.
  • Create pages for authentication, view recipes & saved recipes & create components for login, register page & navigation bar.
  • Also create a hook to get the userID stored in the local storage item.

Steps to create Backend project

Step 1: Start by creating an index.js file and write the basic boiler plate code for node.js & express.js in the Server folder.

Step 2: Install node modules by running the following command:

npm install

Step 3: Install several other required packages for the recipe manager. For eg. Here we have installed the following dependencies: (package.json)

Step 4: Connect the server side application to mongoose. Prefer adding the mongodb url and port number, JWT Secret key in the env file in the following format & access it using process.env.property. This helps in privacy of the details:

MONGODB_URL= '  '
JWT_secret = ' '

Step 5: Add both routes & schema models for recipes & users.

Project Structure

Screenshot-(82)

Server (Backend) project structure

The updated dependencies in package.json file will look like:

"dependencies": {
"bcrypt": "^5.1.1",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.19.1",
"jsonwebtoken": "^9.0.2",
"mongodb": "^2.2.16",
"mongoose": "^8.2.3",
"nodemon": "^3.1.0"
}

Steps to create Frontend project

Step 1: Create a vite project using the following command:

npm create vite@latest

Step 2: Name your project & select the React framework here using the downward arrow key.

Vanilla
Vue
React
Preact
Lit
Svelte
Solid
Qwik
Others

Step 3: Select Variant: choose any variant of your choice using the downward arrow key,i.e: choose JavaScript

TypeScript
TypeScript + SWC
JavaScript
JavaScript + SWC

Step 4: Now, switch to my-react-app directory

cd my-react-app

Here we have installed the following dependencies other than node modules: (package.json)

Project Structure:

Screenshot-(83)

Client (Frontend) project structure

Example: Implementation to write the code for backend part.

JavaScript
// index.js

import express from 'express';
import cors from 'cors';
import mongoose from 'mongoose';
import 'dotenv/config';
import { userRouter } from './Routes/users.js';
import { RecipesRouter } from './Routes/recipes.js';

const app = express();
// Parsing Middleware
app.use(express.json());
app.use(cors());

app.use("/auth", userRouter);

app.use("/recipes", RecipesRouter);

mongoose.connect(process.env.MONGODB_URL);

app.listen(5000, () => {
    console.log('App is listening to PORT 5000 & has been successfully connected to the databse!');
})
JavaScript
// Routes
// users.js

import express from 'express';
import jwt from 'jsonwebtoken';
import bcrypt from 'bcrypt';
import { UserModel } from '../Models/users.js';
import 'dotenv/config';

const router = express.Router();

router.post("/register", async (req, res) => {
    const { username, password } = req.body;
    const user = await UserModel.findOne({ username });

    if (user) {
        return res.json({ message: "User already exists!" });
    }
    else {
        const hashedPwd = await bcrypt.hash(password, 10); // 10 represents number of rounds in hashing process
        const newUser = new UserModel({ username, password: hashedPwd });
        await newUser.save();
    }
    res.json({ message: "User registered successfully!" });
})

router.post("/login", async (req, res) => {
    const { username, password } = req.body;

    const user = await UserModel.findOne({ username });
    if (!user) {
        return res.json({ message: "No such credentials exist!" });
    }

    const isPwdValid = await bcrypt.compare(password, user.password);
    if (!isPwdValid) {
        return res.json({ message: "The password is incorrect!" });
    }

    // JWT - Creates a token of payload (identification data) & secret key
    // Secret Key is used by servers
    // Token is returned to client for future use along with user id
    const token = jwt.sign({ id: user._id }, process.env.JWT_secret);
    res.json({ token, userID: user._id });
    // _id from database
})

export { router as userRouter };
JavaScript
// Routes
// recipes.js

import { RecipeModel } from "../Models/recipes.js";
import { UserModel } from "../Models/users.js";
import express from 'express';

const router = express.Router();

router.get('/', async (request, response) => {
    try {
        const recipes = await RecipeModel.find({});
        response.json(recipes);
    } catch (error) {
        response.json(error);
    }
});

router.post('/', async (request, response) => {
    const newRecipe = new RecipeModel(request.body);
    try {
        const resp = await newRecipe.save();
        response.json(resp);
    } catch (error) {
        response.json(error);
    }
});

// Save a Recipe
router.put("/", async (req, res) => {
    const recipe = await RecipeModel.findById(req.body.recipeID);
    const user = await UserModel.findById(req.body.userID);
    try {
        user.savedRecipes.push(recipe);
        await user.save();
        res.status(201).json({ savedRecipes: user.savedRecipes });
    } catch (err) {
        res.status(500).json(err);
    }
});

// Get id of saved recipes
router.get("/savedRecipes/ids/:userId", async (req, res) => {
    try {
        const user = await UserModel.findById(req.params.userId);
        res.status(201).json({ savedRecipes: user?.savedRecipes });
    } catch (err) {
        console.log(err);
        res.status(500).json(err);
    }
});

// Get saved recipes
router.get("/savedRecipes/:userId", async (req, res) => {
    try {
        const user = await UserModel.findById(req.params.userId);
        const savedRecipes = await RecipeModel.find({
            _id: { $in: user.savedRecipes },
        });

        console.log(savedRecipes);
        res.status(201).json({ savedRecipes });
    } catch (err) {
        console.log(err);
        res.status(500).json(err);
    }
});

export { router as RecipesRouter };
JavaScript
// Models
// users.js

import mongoose from 'mongoose';
const { Schema } = mongoose;

const userSchema = new Schema({
    username: {
        type: String,
        required: true,
        unique: true
    },
    password: {
        type: String,
        required: true
    },
    savedRecipes: [
        {
            type: mongoose.Schema.Types.ObjectId,
            ref: "recipes"
        }
    ]
});

export const UserModel = mongoose.model('users', userSchema);
JavaScript
// Models
// recipes.js

import mongoose from 'mongoose';
const { Schema } = mongoose;

const RecipeSchema = new Schema({
    name: {
        type: String,
        required: true
    },
    ingredients: [
        {
            type: String,
            required: true
        },
    ],
    instructions: {
        type: String,
        required: true,
    },
    imageUrl: {
        type: String,
        required: true,
    },
    cookingTime: {
        type: Number,
        required: true,
    },
    userOwner: {
        type: mongoose.Schema.Types.ObjectId, // ObjectId provided by Mongoose(MongoDB)
        ref: "users",
        required: true,
    },
});

export const RecipeModel = mongoose.model('recipes', RecipeSchema);


Example: Implementation to write the frontend code for the above project.

CSS
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
  font-family: Poppins;
}

@tailwind base;
@tailwind components;
@tailwind utilities;
JavaScript
// App.jsx

import React from 'react'
import { Route, Routes } from 'react-router-dom'
import Home from './Pages/Home.jsx'
import Auth from './Pages/Auth.jsx'
import Saved from './Pages/Saved.jsx'
import Create from './Pages/Create.jsx'

const App = () => {
  return (
    <>
      <Routes>
        <Route path='/' element={<Home />} ></Route>
        <Route path='/auth' element={<Auth />} ></Route>
        <Route path='/saved' element={<Saved />} ></Route>
        <Route path='/create' element={<Create />} ></Route>
      </Routes>
    </>
  )
}

export default App
JavaScript
// main.jsx

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import './index.css'
import { BrowserRouter } from 'react-router-dom'
import NavBar from './Components/NavBar.jsx'

ReactDOM.createRoot(document.getElementById('root')).render(
  <BrowserRouter>
    <NavBar />
    <App />
  </BrowserRouter>,
)
JavaScript
// Pages
// Auth.jsx

import React from 'react';
import Login from '../Components/Login';
import Register from '../Components/Register';

const Auth = () => {
    return (
        <div className='flex items-center justify-center'>
            <Register />
            <Login />
        </div>
    )
}

export default Auth
JavaScript
// Pages
// Create.jsx

import React, { useState } from 'react';
import axios from 'axios';
import { toast, ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import { useGetUserID } from '../Hooks/useGetUserID.jsx';

const CreateRecipe = () => {

    const userID = useGetUserID();

    const [recipe, setRecipe] = useState({
        name: '',
        ingredients: [],
        instructions: '',
        imageUrl: '',
        cookingTime: 0,
        userOwner: userID
    });


    const handleChange = (event) => {
        const { name, value } = event.target; // access name and value of the occurring event
        setRecipe({ ...recipe, [name]: value }); // set the previous recipe state's recipe's name with user's entered new value
    }


    const addIngredients = () => {
        setRecipe({ ...recipe, ingredients: [...recipe.ingredients, ""] }); // updates the previous recipe state
        // ingredients with an empty string
        // making it possible to store as many ingredients as possible
    }


    const handleIngredientChange = (event, index) => {
        const value = event.target.value; // user's entered value
        // const {value} = evetn.target; (Alternative)
        const ingredients = recipe.ingredients; // get the previous array of ingredients from recipe state
        ingredients[index] = value; // store the user entered ingredient at the index of array
        setRecipe({ ...recipe, ingredients: ingredients }); // set the previous state of recipe with new ingredients
    }


    const submitRecipe = async (e) => {
        e.preventDefault();
        try {
            await axios.post('https://recipe-application-hcbe.onrender.com/recipes', recipe);
            toast.success('Recipe Created Successfully ?');
        } catch (error) {
            console.log(error.message);
            toast.error('Recipe could not be created ?')
        }
    }

    return (
        <div className="max-w-md mx-auto mt-8 p-6 bg-white rounded-lg shadow-md">
            <h1 className="text-2xl font-semibold mb-6">Create Recipe</h1>
            <form>
                <div className="mb-4">
                    <label htmlFor="name" className="block text-gray-700 mb-2">Name</label>
                    <input
                        type="text"
                        id="name"
                        name="name"
                        className="w-full px-3 py-2 border rounded-md focus:outline-none focus:border-blue-500"
                        required
                        onChange={handleChange}
                    />
                </div>
                <div className="mb-4">
                    <label htmlFor="ingredients" className="block text-gray-700 mb-2">Ingredients</label>
                    {recipe.ingredients.map((ingredient, index) => (
                        <input
                            key={index}
                            className="w-full px-3 py-2 border rounded-md
                             focus:outline-none focus:border-blue-500 mt-3"
                            type='text'
                            name='ingredients'
                            value={ingredient}
                            onChange={(event) => handleIngredientChange(event, index)}
                        />
                    ))}
                    <button
                        type='button'
                        onClick={addIngredients}
                        className="w-full bg-blue-500 text-white py-2 rounded-md hover:bg-blue-600 my-5"
                    >Add Ingredients</button>
                </div>
                <div className="mb-4">
                    <label htmlFor="instructions" className="block text-gray-700 mb-2">Instructions</label>
                    <textarea
                        id="instructions"
                        name="instructions"
                        className="w-full px-3 py-2 border rounded-md focus:outline-none focus:border-blue-500"
                        required
                        onChange={handleChange}
                    ></textarea>
                </div>
                <div className="mb-4">
                    <label htmlFor="imageUrl" className="block text-gray-700 mb-2">Image URL</label>
                    <input
                        type="text"
                        id="imageUrl"
                        name="imageUrl"
                        className="w-full px-3 py-2 border rounded-md focus:outline-none focus:border-blue-500"
                        required
                        onChange={handleChange}
                    />
                </div>
                <div className="mb-4">
                    <label htmlFor="cookingTime" className="block text-gray-700 mb-2">Cooking Time</label>
                    <input
                        type="number"
                        id="cookingTime"
                        name="cookingTime"
                        className="w-full px-3 py-2 border rounded-md focus:outline-none focus:border-blue-500"
                        required
                        onChange={handleChange}
                    />
                </div>
                <button
                    type="submit"
                    className="w-full bg-blue-500 text-white py-2 rounded-md hover:bg-blue-600"
                    onClick={(e) => submitRecipe(e)}
                >Create Recipe</button>
            </form>
            <ToastContainer />
        </div>
    );
};

export default CreateRecipe;
JavaScript
// Home.jsx

// import React, { useState, useEffect } from 'react';
import axios from 'axios';
import { useGetUserID } from '../Hooks/useGetUserID.jsx';

const Home = () => {

    const [recipes, setRecipes] = useState([]);
    const [savedRecipes, setSavedRecipes] = useState([]);

    let userID = useGetUserID();

    if(userID === null){
        userID = "65ffee9462a30d065b80f9fc";
    }

    useEffect(() => {
        const fetchRecipes = async () => {
            try {
                const response = await axios.get(
"https://recipe-application-hcbe.onrender.com/recipes");
                setRecipes(response.data);
            } catch (err) {
                console.log(err);
            }
        };

        const fetchSavedRecipes = async () => {
            try {
                const response = await axios.get(
`https://recipe-application-hcbe.onrender.com/recipes/savedRecipes/ids/${userID}`
                );
                setSavedRecipes(response.data.savedRecipes);
            } catch (err) {
                console.log(err);
            }
        };

        fetchRecipes();
        fetchSavedRecipes();
    }, []);

    const saveRecipe = async (recipeID) => {
        try {
            const response = await axios.put(
"https://recipe-application-hcbe.onrender.com/recipes", {
                recipeID,
                userID,
            });
            setSavedRecipes(response.data.savedRecipes);
        } catch (err) {
            console.log(err);
        }
    };

    const isRecipeSaved = (id) => savedRecipes.includes(id);

    return (
        <div className="container mx-auto px-4">
            <h1 className="text-3xl font-bold m-12 text-center">
                Recipes
            </h1>
            {!userID ?
                (<p className='text-2xl font-semibold 
                    my-8 text-center text-[#4b0097]'>
                    Login or Register to create your 
                    own recipes and save them for later!
                </p>)
                :
                (<p></p>)
            }

            <ul className="grid grid-cols-1 sm:grid-cols-2 
                           lg:grid-cols-3 gap-6 m-10 ">
                {recipes.map((recipe) => (
                    <li key={recipe._id} className="border border-gray-300 
                                                    rounded-md p-4 shadow-lg">
                        <img src={recipe.imageUrl} alt={recipe.name} 
                             className="w-full h-60 object-cover 
                                         mb-4 rounded-md" />
                        <div className="flex justify-between items-center mb-2">
                            <h2 className="text-lg font-semibold">{recipe.name}</h2>
                            <button
                                className="bg-blue-500 text-white 
                                           px-4 py-2 rounded-md hover:bg-blue-600"
                                onClick={() => saveRecipe(recipe._id)}
                                disabled={isRecipeSaved(recipe._id)}>
                                {isRecipeSaved(recipe._id) ? "Saved" : "Save"}

                            </button>
                        </div>
                        <p className="text-gray-600 my-4">{recipe.instructions}</p>
                        <p className="text-gray-900 font-semibold">
                            Cooking Time: (in minutes) {recipe.cookingTime} minutes
                        </p>
                    </li>
                ))}
            </ul>
        </div>
    );
};

export default Home;
JavaScript
// Pages
// Saved.jsx

import React, { useState, useEffect } from 'react';
import axios from 'axios';
import { useGetUserID } from '../Hooks/useGetUserID.jsx';

const Saved = () => {

  const [savedRecipes, setSavedRecipes] = useState([]);

  let userID = useGetUserID();

  useEffect(() => {

    const fetchSavedRecipes = async () => {
      try {
        const response = await axios.get(
`https://recipe-application-hcbe.onrender.com/recipes/savedRecipes/${userID}`);
        setSavedRecipes(response.data.savedRecipes);
        console.log(response);
      } catch (error) {
        console.log(error.message);
      }
    };

    fetchSavedRecipes();
  }, []);


  return (
    <div className="container mx-auto px-4">
      <h1 className="text-3xl font-bold m-12 text-center">Saved Recipes</h1>
      {!userID ?
        (<p className='text-2xl font-semibold my-8 
                       text-center text-[#4b0097]'>
                       When you create an account, 
                       all the saved recipes will appear here!
        </p>)
        :
        (<p></p>)
      }
      <div className="grid grid-cols-1 sm:grid-cols-2 
                        lg:grid-cols-3 gap-6 m-10 ">
        {savedRecipes.map((recipe) => (
          <div key={recipe._id} className="border border-gray-300 
                                  rounded-md p-4 shadow-lg">
            <img src={recipe.imageUrl} alt={recipe.name} 
                 className="w-full h-60 object-cover mb-4 rounded-md" />
            <div className="flex justify-between items-center mb-2">
              <h2 className="text-lg font-semibold">{recipe.name}</h2>
            </div>
            <p className="text-gray-600 my-4">{recipe.instructions}</p>
            <p className="text-gray-900 font-semibold">
                Cooking Time: (in minutes) {recipe.cookingTime} minutes
            </p>
          </div>
        ))}
      </div>
    </div>
  );
};

export default Saved;
JavaScript
// Hooks
// useGetUserID.jsx

export const useGetUserID = () => {
    return window.localStorage.getItem("userID");
}
JavaScript
// Login.jsx

import React, { useState } from 'react';
import axios from 'axios';
import { useCookies } from 'react-cookie';
import { useNavigate } from 'react-router-dom';
import { toast, ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';

const Login = () => {

    const [username, setUsername] = useState("");
    const [password, setPassword] = useState("");

    const [cookies, setCookies] = useCookies('access_token');
    const navigate = useNavigate();

    const handleSubmit = async (e) => {
        e.preventDefault();
        try {
            const response = await axios.post(
'https://recipe-application-hcbe.onrender.com/auth/login', { username, password, });
            setCookies("access_token", response.data.token);
            window.localStorage.setItem('userID', response.data.userID);
            navigate('/');
        } catch (error) {
            console.error(error.message);
            toast.error('User is not registered?');
        }
    };

    return (
        <div className="flex mx-16 items-center h-screen">
            <form onSubmit={handleSubmit} className="bg-gray-100 p-6 
                                                    rounded-lg shadow-md">
                <h2 className="text-2xl font-semibold mb-6">Login</h2>
                <div className="mb-4">
                    <label htmlFor="username" 
                           className="block text-gray-700">Username</label>
                    <input
                        type="text"
                        className="w-full px-3 py-2 border rounded-md 
                                    focus:outline-none focus:border-blue-500"
                        value={username}
                        onChange={(e) => setUsername(e.target.value)}
                        required
                    />
                </div>
                <div className="mb-4">
                    <label htmlFor="password" 
                            className="block text-gray-700">Password</label>
                    <input
                        type="password"
                        className="w-full px-3 py-2 border rounded-md 
                                    focus:outline-none focus:border-blue-500"
                        value={password}
                        onChange={(e) => setPassword(e.target.value)}
                        required
                    />
                </div>
                <button type="submit" 
                        className="w-full bg-blue-500 
                                    text-white py-2 rounded-md hover:bg-blue-600">
                                    Login
                </button>
            </form>
            <ToastContainer />
        </div>
    );
};

export default Login;
JavaScript
// Components
// NavBar.jsx

import React from 'react';
import { Link } from 'react-router-dom';
import { useCookies } from 'react-cookie';
import { useNavigate } from 'react-router-dom';

const NavBar = () => {

    // Cookies that saves previous data of recipes
    // of the user ( client side )
    const [cookies, setCookies] = useCookies(['access_token']);
    const navigate = useNavigate();

    const logout = () => {
        setCookies('access_token', "");
        window.localStorage.removeItem('userID');
        navigate('/');
    }

    return (
        <nav className="bg-gray-800 py-6 sticky top-0">
            <div className="container mx-auto flex justify-between items-center">
                <Link
                    to="/"
                    className="text-white text-lg font-semibold">
                    Recipe App
                </Link>
                <div className="flex items-center justify-center">
                    <Link
                        to="/create"
                        className="text-white mr-4 hover:text-gray-300">
                        Create Recipes
                    </Link>
                    <Link
                        to="/saved"
                        className="text-white mr-4 hover:text-gray-300">
                        Saved Recipes
                    </Link>
                    {!cookies.access_token ? (<Link to="/auth" className="text-white mr-4 hover:text-gray-300">Authentication</Link>
                    ) : (<button onClick={logout}
                        className='text-white mr-12 hover:text-black
                        border-2 border-white p-2 rounded-xl
                        hover:bg-white delay-75 transition
                        '>
                        Logout
                    </button>)}
                </div>
            </div>
        </nav>
    );
};

export default NavBar;
JavaScript
// Components
// Register.jsx

import React, { useState } from 'react';
import axios from 'axios';
import { toast, ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';

const Register = () => {
    const [username, setUsername] = useState("");
    const [password, setPassword] = useState("");

    const handleSubmit = async (e) => {
        e.preventDefault();
        try {
            await axios.post(
'https://recipe-application-hcbe.onrender.com/auth/register', 
        { username, password });
            toast.success('User Registered Successfully! Now Login ?');
        } catch (error) {
            toast.error('Oops! The user could not be registered! It seems the user already exists ?');
            console.error(error.message);
        }
    };

    return (
        <div className="flex mx-16 items-center h-screen">
            <form onSubmit={(e) => handleSubmit(e)} 
                    className="bg-gray-100 p-6 rounded-lg shadow-md">
                <h2 className="text-2xl font-semibold mb-6">Register</h2>
                <div className="mb-4">
                    <label htmlFor="username" 
                            className="block text-gray-700">Username</label>
                    <input
                        type="text"
                        className="w-full px-3 py-2 border rounded-md 
                                focus:outline-none focus:border-blue-500"
                        value={username}
                        onChange={(e) => setUsername(e.target.value)}
                        required
                    />
                </div>
                <div className="mb-4">
                    <label htmlFor="password" className="block text-gray-700">Password</label>
                    <input
                        type="password"
                        className="w-full px-3 py-2 border rounded-md 
                                focus:outline-none focus:border-blue-500"
                        value={password}
                        onChange={(e) => setPassword(e.target.value)}
                        required
                    />
                </div>
                <button type="submit" className="w-full bg-blue-500 
                            text-white py-2 rounded-md hover:bg-blue-600">
                            Register
                </button>
            </form>
            <ToastContainer />
        </div>
    );
};

export default Register;


Step to Run Application: Run the application using the following command from the root directory of the project

npm start

Output: Your project will be shown in the URL http://localhost:3000/




Reffered: https://www.geeksforgeeks.org


Project

Related
Build a Music app using VueJS Build a Music app using VueJS
Word Cloud Generator from Given Passage Word Cloud Generator from Given Passage
Notes Maker App using MEAN Stack Notes Maker App using MEAN Stack
30 Scratch Project Ideas for Kids (Age 5 - 15) &amp; Beginners 30 Scratch Project Ideas for Kids (Age 5 - 15) &amp; Beginners
TIF to JPG Converter TIF to JPG Converter

Type:
Geek
Category:
Coding
Sub Category:
Tutorial
Uploaded by:
Admin
Views:
18