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:
-compressed.jpg) Recipe Manager PrerequisitesApproach- 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 projectStep 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.png) 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 projectStep 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:.png) 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/
|