In the fast world where there is no time for people to cook food or go to restaurants to eat, Food Delivery applications are one of the best options for them. In this tutorial, you will learn how to create a simple food delivery app using MERN stack. Our application will allow users to browse through a list of restaurants, view their menus, and add items to a shopping cart.
Preview of final output: Let us have a look at how the final output will look like
.jpg)
Prerequisites:
Approach to create Restaurant App using MERN:
1. Import Statements:
- Import necessary dependencies and components.
- React is imported for defining React components.
- RestaurantList, RestaurantCard, DishesMenu, DishCard and Cart are custom components, assumed to be present in the ./components directory.
- RestaurantContext is imported, presumably a custom context provider.
2.Functional Component:
- Define a functional component named App.
3.Context Provider:
- Wrap the App component inside the RestaurantContext provider. This suggests that the components within this provider have access to the context provided by RestaurantContext.
4.Component Rendering:
- Render the following components:
- RestaurantContext: Presumably, this is a context provider that wraps its child components (App). The purpose of this context is not clear from the provided code snippet.
- All other components such as RestaurantList and DishesMenu is wrapped inside App component so it also has the access of RestaurantContext.
- RestaurantList wraps RestaurantCard
Steps to create Application:
Step 1: creating the folder for the project
mkdir food-delivery-app cd food-delivery-app
Step 2: Create a backend using the following commands
mkdir backend cd backend npm init -y
Step 3: Install the required dependencies.
npm i cors express mongoose nodemon
Project Structure(Backend):
 Backend Folder
Backend Dependencies:
"dependencies": { "cors": "^2.8.5", "express": "^4.18.2", "mongoose": "^8.0.3", "nodemon": "^3.0.2" }
Example: Create server.js file and add the following code.
Javascript
const express = require( "express" );
const mongoose = require( "mongoose" );
const cors = require( "cors" );
const app = express();
const PORT = process.env.PORT || 5000;
app.use(cors());
app.use(express.json());
mongoose
useNewUrlParser: true ,
useUnifiedTopology: true ,
})
.then(() => console.log( "Connected to db" ))
. catch ((err) => console.log( "Error connecting to db" , err));
const restaurantSchema = new mongoose.Schema({
name: String,
image: String,
menu: [
{
name: String,
price: Number,
image: String,
},
],
rating: Number,
});
const Restaurant = mongoose.model( "Restaurant" , restaurantSchema);
const previousOrderSchema = new mongoose.Schema({
orderId: { type: String, required: true },
dateOfOrder: { type: Date, required: true },
amount: { type: Number, required: true },
});
const PreviousOrder = mongoose.model( "PreviousOrder" , previousOrderSchema);
const seedData = [
{
name: "Italian Delight" ,
menu: [
{
name: "Pasta Alfredo" ,
price: 10,
image:
},
{
name: "Margherita Pizza" ,
price: 15,
image:
},
{
name: "Chicken Parmesan" ,
price: 20,
image:
},
],
rating: 4.5,
},
{
name: "Seafood Paradise" ,
image:
menu: [
{
name: "Grilled Salmon" ,
price: 12,
image:
},
{
name: "Lobster Bisque" ,
price: 18,
image:
},
{
name: "Shrimp Scampi" ,
price: 25,
image:
},
],
rating: 3.8,
},
{
name: "Vegetarian Haven" ,
menu: [
{
name: "Quinoa Salad" ,
price: 8,
image:
},
{
name: "Eggplant Parmesan" ,
price: 12,
image:
},
{
name: "Mushroom Risotto" ,
price: 16,
image:
},
],
rating: 4.2,
},
{
name: "Sizzling Steakhouse" ,
menu: [
{
name: "Filet Mignon" ,
price: 22,
},
{
name: "New York Strip" ,
price: 18,
},
{
name: "Ribeye Steak" ,
price: 25,
},
],
rating: 4.7,
},
{
name: "Asian Fusion" ,
menu: [
{
name: "Sushi Platter" ,
price: 20,
},
{
name: "Pad Thai" ,
price: 15,
},
{
name: "Mongolian Beef" ,
price: 18,
},
],
rating: 4.0,
},
];
const seedDatabase = async () => {
try {
await Restaurant.deleteMany();
await Restaurant.insertMany(seedData);
console.log( "Database seeded successfully." );
} catch (error) {
console.error( "Error seeding the database:" , error.message);
}
};
seedDatabase();
const insertDummyData = async () => {
try {
const existingOrders = await PreviousOrder.find();
if (existingOrders.length === 0) {
const dummyOrders = [
{ orderId: "001" , dateOfOrder: new Date(), amount: 30 },
{ orderId: "002" , dateOfOrder: new Date(), amount: 45 },
];
await PreviousOrder.insertMany(dummyOrders);
console.log( "Dummy data inserted successfully!" );
}
} catch (error) {
console.error( "Error inserting dummy data:" , error);
}
};
insertDummyData();
app.get( "/restaurants" , async (req, res) => {
try {
const restaurants = await Restaurant.find({});
res.json(restaurants);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.get( "/previousOrders" , async (req, res) => {
try {
const orders = await PreviousOrder.find();
res.status(200).json(orders);
} catch (error) {
res.status(500).json({ error: "Internal server error" });
}
});
app.post( "/previousOrders" , async (req, res) => {
try {
const { orderId, dateOfOrder, amount } = req.body;
console.log(orderId, dateOfOrder, amount);
const newOrder = new PreviousOrder({
orderId,
dateOfOrder: new Date(dateOfOrder),
amount,
});
await newOrder.save();
res.status(201).json({ message: "Dummy order saved successfully!" });
} catch (error) {
res.status(500).json({ error: "Internal server error" });
}
});
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
|
Step 4: To start the backend run the following command.
nodemon server.js
Step 5: Go to the root directory of the application and create the frontend application.
npx create-react-app client cd client
Step 6: Install important dependencies: axios
npm i axios
Project Structure(Frontend):
 Frontend Folder Structure.
Frontend dependencies:
"dependencies": { "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "axios": "^1.6.5", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.21.1", "react-scripts": "5.0.1", "web-vitals": "^2.1.4" }
Example: Create the required files and add the following code.
Javascript
import React from 'react' ;
import ReactDOM from 'react-dom/client' ;
import './index.css' ;
import App from './App' ;
import reportWebVitals from './reportWebVitals' ;
import { RestaurantProvider } from './contexts/RestaurantContext' ;
const root = ReactDOM.createRoot(document.getElementById( 'root' ));
root.render(
<RestaurantProvider>
<App />
</RestaurantProvider>
);
reportWebVitals();
|
Javascript
import React, { useContext } from "react" ;
import RestaurantList from "./components/RestaurantList" ;
import DishesMenu from "./components/DishesMenu" ;
import Cart from "./components/Cart" ;
import { RestaurantContext } from "./contexts/RestaurantContext" ;
import "./App.css" ;
const App = () => {
const { selectedRestaurant } = useContext(RestaurantContext);
return (
<>
<div className= "container" >
<h1 className= "header" >GFG Food Delivery App</h1>
<Cart
style={{ position: "absolute" , right: "20px" , top: "20px" }}
/>
<RestaurantList />
{selectedRestaurant && <DishesMenu />}
</div>
</>
);
};
export default App;
|
Javascript
import React, { createContext, useState, useEffect } from "react" ;
import axios from "axios" ;
const RestaurantContext = createContext();
const RestaurantProvider = ({ children }) => {
const [restaurants, setRestaurants] = useState([]);
const [selectedRestaurant, setSelectedRestaurant] = useState( null );
const [cartItems, setCartItems] = useState([]);
const [totalPrice, setTotalPrice] = useState(0);
useEffect(() => {
const fetchRestaurants = async () => {
try {
const response = await axios.get(
);
setRestaurants(response.data);
} catch (error) {
console.error( "Error fetching restaurants:" , error.message);
}
};
fetchRestaurants();
}, []);
const handleAddItems = (dish) => {
console.log( "Dish:" , dish);
const existingItemIndex = cartItems.findIndex(
(item) => item._id === dish._id
);
if (existingItemIndex !== -1) {
console.log(
"Dish already exists in the cart.
You may want to update the quantity."
);
const updatedCartItems = [...cartItems];
updatedCartItems[existingItemIndex] = {
...updatedCartItems[existingItemIndex],
quantity: updatedCartItems[existingItemIndex].quantity + 1,
};
setCartItems(updatedCartItems);
} else {
console.log( "Dish does not exist in the cart. Adding to the cart." );
console.log( "cart" , cartItems.length);
setCartItems([...cartItems, { ...dish, quantity: 1 }]);
}
setTotalPrice((prev) => prev + dish.price);
};
const handleRemoveItems = (dish) => {
console.log( "Dish ID to remove:" , dish);
const existingItemIndex = cartItems.findIndex(
(item) => item._id === dish._id
);
if (existingItemIndex !== -1) {
console.log(
"Dish exists in the cart. You may
want to decrease the quantity or remove it."
);
const updatedCartItems = [...cartItems];
if (updatedCartItems[existingItemIndex].quantity > 1) {
updatedCartItems[existingItemIndex] = {
...updatedCartItems[existingItemIndex],
quantity: updatedCartItems[existingItemIndex].quantity - 1,
};
setTotalPrice(totalPrice - cartItems[existingItemIndex].price);
} else {
updatedCartItems.splice(existingItemIndex, 1);
setTotalPrice(totalPrice - cartItems[existingItemIndex].price);
}
setCartItems(updatedCartItems);
} else {
console.log( "Dish does not exist in the cart." );
}
};
const emptyCart = () => {
setCartItems([]);
setTotalPrice(0);
};
const value = {
restaurants,
selectedRestaurant,
setSelectedRestaurant,
handleAddItems,
handleRemoveItems,
totalPrice,
emptyCart,
};
return (
<RestaurantContext.Provider value={value}>
{children}
</RestaurantContext.Provider>
);
};
export { RestaurantContext, RestaurantProvider };
|
Javascript
import React, { useContext, useState } from "react" ;
import axios from "axios" ;
import { RestaurantContext } from "../contexts/RestaurantContext" ;
const Cart = () => {
const { totalPrice, emptyCart } = useContext(RestaurantContext);
const [isCheckingOut, setIsCheckingOut] = useState( false );
const generateOrderId = () => {
return `${Math.floor(Math.random() * 1000)}`;
};
const handleCheckout = async () => {
try {
setIsCheckingOut( true );
const orderId = generateOrderId();
const response = await axios.post(
{
orderId,
dateOfOrder: new Date(),
amount: totalPrice,
}
);
console.log(response.data);
emptyCart();
} catch (error) {
console.error( "Error during checkout:" , error.message);
} finally {
setIsCheckingOut( false );
}
};
return (
<div className= "cart-container" >
<h2>Cart</h2>
<div className= "cart-content" >
<span style={{ color: "brown" }}>Total Price: </span> $
{totalPrice}
<button onClick={handleCheckout} disabled={isCheckingOut}>
{isCheckingOut ? "Checking out..." : "Checkout" }
</button>
</div>
</div>
);
};
export default Cart;
|
Javascript
import React, { useContext } from "react" ;
import { RestaurantContext } from "../contexts/RestaurantContext" ;
const DishCard = ({ dish }) => {
const { handleAddItems, handleRemoveItems } = useContext(RestaurantContext);
const handleAdd = () => {
handleAddItems(dish);
};
const handleRemove = () => {
handleRemoveItems(dish);
};
return (
<div className= "dish-card" >
<h3>{dish.name}</h3>
<img src={dish.image} alt= "" />
<p>Price: ${dish.price}</p>
<div
style={{
width: "40%" ,
display: "flex" ,
justifyContent: "space-between" ,
alignItems: "center" ,
}}
>
<button onClick={handleAdd}>+</button>
<button onClick={handleRemove}>-</button>
</div>
</div>
);
};
export default DishCard;
|
Javascript
import React, { useContext } from 'react' ;
import DishCard from './DishCard' ;
import { RestaurantContext } from '../contexts/RestaurantContext' ;
const DishesMenu = () => {
const { selectedRestaurant } = useContext(RestaurantContext);
return (
<div>
<h2>Menu</h2>
{selectedRestaurant && (
<div style={{ display: 'flex' , flexWrap: 'wrap' }}>
{selectedRestaurant.menu.map((dish) => (
<DishCard key={dish.name} dish={dish} />
))}
</div>
)}
</div>
);
};
export default DishesMenu;
|
Javascript
import React, { useState, useEffect } from 'react' ;
import axios from 'axios' ;
const PreviousOrders = ({ handleShow }) => {
const [orders, setOrders] = useState([]);
useEffect(() => {
const fetchOrders = async () => {
try {
setOrders(response.data);
} catch (error) {
console.error( 'Error fetching orders:' , error.message);
}
};
fetchOrders();
}, []);
return (
<div className= "previous-orders-container" >
<h2>Your Previous Orders</h2>
<button style={{ backgroundColor: "white" , color: "red" }} onClick={handleShow}>Close</button>
<ul className= "orders-list" >
{orders.map(order => (
<li key={order.orderId} className= "order-card" >
<h3>Order #{order.orderId}</h3>
<div className= "order-details" >
<div>Items: 1</div>
<div>Total Amount: ${order.amount.toFixed(2)}</div>
</div>
<div>Ordered on: { new Date(order.dateOfOrder).toLocaleDateString()}</div>
</li>
))}
</ul>
</div>
);
};
export default PreviousOrders;
|
Javascript
import React from 'react' ;
const RestaurantCard = ({ restaurant, onClick }) => {
return (
<div className= "card" onClick={onClick}>
<h3>{restaurant.name}</h3>
<div className= "image-container" >
<img className= "restaurant-image" src={restaurant.image} alt={restaurant.name} />
</div>
<p>Rating: {restaurant.rating}</p>
</div>
);
};
export default RestaurantCard;
|
Javascript
import React, { useContext, useState, useEffect } from 'react' ;
import RestaurantCard from './RestaurantCard' ;
import { RestaurantContext } from '../contexts/RestaurantContext' ;
import PreviousOrders from './PreviousOders' ;
const RestaurantList = () => {
const { restaurants, setSelectedRestaurant } = useContext(RestaurantContext);
const [filteredRestaurants, setFilteredRestaurants] = useState([...restaurants]);
const [ratingFilter, setRatingFilter] = useState( '' );
const [searchTerm, setSearchTerm] = useState( '' );
const [showOrder, setShowOrder] = useState( false )
useEffect(() => {
filterRestaurants();
}, [ratingFilter, searchTerm, restaurants]);
const handleRestaurantClick = (restaurantId) => {
setSelectedRestaurant(restaurants.find((restaurant) => restaurant._id === restaurantId));
};
const handleRatingChange = (e) => {
setRatingFilter(e.target.value);
};
const handleSearchChange = (e) => {
setSearchTerm(e.target.value);
};
const filterRestaurants = () => {
let filtered = restaurants;
if (ratingFilter) {
filtered = filtered.filter((restaurant) => restaurant.rating >= parseFloat(ratingFilter));
}
if (searchTerm) {
const searchLower = searchTerm.toLowerCase();
filtered = filtered.filter((restaurant) =>
restaurant.name.toLowerCase().includes(searchLower)
);
}
setFilteredRestaurants(filtered);
};
const handleShow = () => {
setShowOrder(!showOrder)
}
return (
<div className= "container" >
<h2 className= "header" >Restaurant List</h2>
<div className= "filter-container" >
<label htmlFor= "rating" className= "filter-label" >
Filter by Rating:
</label>
<input
type= "number"
id= "rating"
value={ratingFilter}
onChange={handleRatingChange}
className= "filter-input"
/>
<label htmlFor= "search" className= "filter-label" >
Search by Name:
</label>
<input
type= "text"
id= "search"
value={searchTerm}
onChange={handleSearchChange}
className= "filter-input"
/>
<p id= 'pre-orders' onClick={handleShow}>
Previous Orders
</p>
</div>
<div className= "restaurant-card-container" >
{filteredRestaurants.map((restaurant) => (
<RestaurantCard
key={restaurant._id}
restaurant={restaurant}
onClick={() => handleRestaurantClick(restaurant._id)}
/>
))}
</div>
{showOrder && <PreviousOrders handleShow={handleShow} />}
</div>
);
};
export default RestaurantList;
|
CSS
.container {
font-family : 'Arial, sans-serif' ;
padding : 20px ;
display : flex;
flex- direction : column;
align-items: center ;
}
.header {
font-size : 24px ;
margin-bottom : 10px ;
border-radius: 15px ;
}
.container {
font-family : 'Arial, sans-serif' ;
padding : 20px ;
display : flex;
flex- direction : column;
align-items: center ;
}
.header {
font-size : 32px ;
margin-bottom : 20px ;
background-color : #fc0671 ;
color : white ;
padding : 10px ;
}
.filter-container {
display : flex;
justify- content : center ;
align-items: center ;
gap: 20px ;
margin-bottom : 20px ;
}
.filter-container label {
font-size : 18px ;
color : #555 ;
}
.filter-input {
padding : 8px ;
font-size : 16px ;
}
.restaurant-card-container {
display : flex;
flex-wrap: wrap;
gap: 20px ;
justify- content : center ;
}
.card {
border : 1px solid #ccc ;
border-radius: 8px ;
padding : 12px ;
margin : 10px ;
width : 200px ;
cursor : pointer ;
transition: transform 0.3 s ease-in-out;
}
.card:hover {
transform: scale( 1.05 );
}
.image-container {
overflow : hidden ;
border-radius: 8px ;
margin-bottom : 10px ;
height : 150px ;
}
.restaurant-image {
width : 100% ;
height : 100% ;
object-fit: cover;
border-radius: 8px ;
}
@media screen and ( max-width : 600px ) {
.card {
width : 100% ;
}
}
.dish-card {
border : 1px solid #ccc ;
border-radius: 8px ;
padding : 12px ;
margin : 10px ;
width : 200px ;
display : flex;
flex- direction : column;
align-items: center ;
text-align : center ;
transition: transform 0.3 s ease-in-out;
}
.dish-card:hover {
transform: scale( 1.05 );
}
img {
width : 100% ;
height : 100px ;
object-fit: cover;
border-radius: 8px ;
margin-bottom : 10px ;
}
h 3 {
margin-bottom : 8px ;
}
p {
margin-bottom : 8px ;
}
button {
margin-top : 8px ;
padding : 8px ;
cursor : pointer ;
background-color : #4caf50 ;
color : white ;
border : none ;
border-radius: 4px ;
}
@media screen and ( max-width : 600px ) {
.dish-card {
width : 100% ;
}
}
.cart-container {
position : fixed ;
top : 10px ;
right : 10px ;
width : 200px ;
border : 2px solid #fc0671 ;
border-radius: 10px ;
height : fit-content;
padding : 5px 10px ;
background-color : #fff ;
box-shadow: 0 0 10px rgba( 0 , 0 , 0 , 0.1 );
overflow-y: auto ;
z-index : 1000 ;
}
.cart-container button {
background-color : #fc0671 ;
color : white ;
border : none ;
border-radius: 10px ;
}
h 2 {
margin-bottom : 10px ;
margin-left : 20px ;
}
.cart-content {
padding : 16px ;
display : flex;
flex- direction : column;
justify- content : center ;
align-items: flex-start;
}
#pre-orders {
background-color : #fc0671 ;
color : aliceblue;
font-size : 20px ;
padding : 5px 10px ;
border-radius: 10px ;
cursor : pointer ;
}
.previous-orders-container {
max-width : 600px ;
margin : 20px auto ;
padding : 20px ;
position : absolute ;
background-color : #fc0671 ;
color : white ;
border-radius: 10px ;
}
.orders-list {
list-style : none ;
padding : 0 ;
}
.order-card {
background-color : #fff ;
border : 1px solid #ddd ;
border-radius: 8px ;
padding : 15px ;
margin-bottom : 15px ;
box-shadow: 0 4px 8px rgba( 0 , 0 , 0 , 0.1 );
transition: transform 0.3 s ease-in-out;
}
.order-card:hover {
transform: scale( 1.02 );
}
.order-card h 3 {
color : #333 ;
margin-bottom : 10px ;
}
.order-details {
display : flex;
justify- content : space-between;
font-size : 14px ;
color : #666 ;
}
|
Step 7: To start the frontend run the following command.
npm start
Output:
.gif) Final Output
|