Creating a Restaurant app will cover a lot of features of the MERN stack. In this tutorial, we’ll guide you through the process of creating a restaurant application using the MERN stack. The 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.
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 the Project
Step 1: creating a project folder.
mkdir restaurant-app
cd restaurant-app
Step 2: Backend Setup
Create folder within restaurant-app folder i.e. server
mkdir server
cd server
Step 3: Initialize the Express project and install the required dependencies.
npm init -y
npm i express cors mongoose
Folder Structure(Backend):
The updated dependencies in package.json file of backend will look like:
"dependencies": {
"cors": "^2.8.5",
"express": "^4.18.2",
"mongoose": "^8.0.4",
"nodemon": "^3.0.2"
}
Example: Now write the codes in the respective files.
//server.js 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.connect( "Your mongodb connection string" , {
useNewUrlParser: true ,
useUnifiedTopology: true ,
}); const restaurantSchema = new mongoose.Schema({
name: String,
image: String,
menu: [
{
name: String,
price: Number,
image: String,
},
],
rating: Number,
}); const Restaurant = mongoose.model( "Restaurant" , restaurantSchema);
// Seed initial data const seedData = [ {
name: "Italian Delight" ,
image: "https://media.geeksforgeeks.org/wp-content/uploads/20240110004602/pexels-chan-walrus-958545-(1).jpg" ,
menu: [
{
name: "Pasta Alfredo" ,
price: 10,
},
{
name: "Margherita Pizza" ,
price: 15,
},
{
name: "Chicken Parmesan" ,
price: 20,
},
],
rating: 4.5,
},
{
name: "Seafood Paradise" ,
image: "https://media.geeksforgeeks.org/wp-content/uploads/20240110004602/pexels-chan-walrus-958545-(1).jpg" ,
menu: [
{
name: "Grilled Salmon" ,
price: 12,
image: "https://media.geeksforgeeks.org/wp-content/uploads/20240110004602/pexels-chan-walrus-958545-(1).jpg" ,
},
{
name: "Lobster Bisque" ,
price: 18,
image: "https://media.geeksforgeeks.org/wp-content/uploads/20240110004602/pexels-chan-walrus-958545-(1).jpg" ,
},
{
name: "Shrimp Scampi" ,
price: 25,
image: "https://media.geeksforgeeks.org/wp-content/uploads/20240110004602/pexels-chan-walrus-958545-(1).jpg" ,
},
],
rating: 3.8,
},
{
name: "Vegetarian Haven" ,
image: "https://media.geeksforgeeks.org/wp-content/uploads/20240110004602/pexels-chan-walrus-958545-(1).jpg" ,
menu: [
{
name: "Quinoa Salad" ,
price: 8,
image: "https://media.geeksforgeeks.org/wp-content/uploads/20240110004602/pexels-chan-walrus-958545-(1).jpg" ,
},
{
name: "Eggplant Parmesan" ,
price: 12,
image: "https://media.geeksforgeeks.org/wp-content/uploads/20240110004602/pexels-chan-walrus-958545-(1).jpg" ,
},
{
name: "Mushroom Risotto" ,
price: 16,
image: "https://media.geeksforgeeks.org/wp-content/uploads/20240110004602/pexels-chan-walrus-958545-(1).jpg" ,
},
],
rating: 4.2,
},
{
name: "Sizzling Steakhouse" ,
image: "https://media.geeksforgeeks.org/wp-content/uploads/20240110004602/pexels-chan-walrus-958545-(1).jpg" ,
menu: [
{
name: "Filet Mignon" ,
price: 22,
image: "https://media.geeksforgeeks.org/wp-content/uploads/20240110004602/pexels-chan-walrus-958545-(1).jpg" ,
},
{
name: "New York Strip" ,
price: 18,
image: "https://media.geeksforgeeks.org/wp-content/uploads/20240110004602/pexels-chan-walrus-958545-(1).jpg" ,
},
{
name: "Ribeye Steak" ,
price: 25,
image: "https://media.geeksforgeeks.org/wp-content/uploads/20240110004602/pexels-chan-walrus-958545-(1).jpg" ,
},
],
rating: 4.7,
},
{
name: "Asian Fusion" ,
image: "https://media.geeksforgeeks.org/wp-content/uploads/20240110004602/pexels-chan-walrus-958545-(1).jpg" ,
menu: [
{
name: "Sushi Platter" ,
price: 20,
image: "https://media.geeksforgeeks.org/wp-content/uploads/20240110004602/pexels-chan-walrus-958545-(1).jpg" ,
},
{
name: "Pad Thai" ,
price: 15,
image: "https://media.geeksforgeeks.org/wp-content/uploads/20240110004602/pexels-chan-walrus-958545-(1).jpg" ,
},
{
name: "Mongolian Beef" ,
price: 18,
image: "https://media.geeksforgeeks.org/wp-content/uploads/20240110004602/pexels-chan-walrus-958545-(1).jpg" ,
},
],
rating: 4.0,
},
]; const seedDatabase = async () => { try {
await Restaurant.deleteMany(); // Clear existing data
await Restaurant.insertMany(seedData);
console.log( "Database seeded successfully." );
} catch (error) {
console.error( "Error seeding the database:" , error.message);
}
}; // Seed data when the server starts seedDatabase(); app.get( "/restaurants" , async (req, res) => {
try {
// Use the 'find' method of the 'Restaurant' model to retrieve all restaurants
const restaurants = await Restaurant.find({});
// Send the retrieved restaurants as a JSON response
res.json(restaurants);
} catch (error) {
// Handle any errors that may occur during the process and send a 500 Internal Server Error response
res.status(500).json({ error: error.message });
}
}); app.listen(PORT, () => { console.log(`Server is running on port ${PORT}`);
}); |
Step 4: Initialize the frontend app and install required dependencies.
npx create-react-app client
cd client
npm i axios
Project Structure (Frontend):
The updated dependencies in package.json file of frontend will look like:
"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-query": "^3.39.3",
"react-router-dom": "^6.21.1",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
}
Example: Create the respective folders and files according to the folder strure give and add the following codes.
// App.js 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' ; // Import the CSS file
const App = () => { const { selectedRestaurant } = useContext(RestaurantContext);
return (
<>
<div className= "container" >
<h1 className= "header" >GFG Restaurant App</h1>
<Cart style={{position: "absolute" ,right: "20px" ,top: "20px" }} />
<RestaurantList />
{selectedRestaurant && <DishesMenu />}
</div>
</>
);
}; export default App;
|
//index.js (update previous index.js) 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(); |
//contexts/RestaurantContext.js 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);
// Check if the dish already exists in the cart
const existingItemIndex = cartItems.findIndex(
(item) => item._id === dish._id
);
if (existingItemIndex !== -1) {
// If the dish already exists, update the
// quantity or any other logic
console.log(
`Dish already exists in the cart.
You may want to update the quantity.`
);
// Example: Increment the quantity
const updatedCartItems = [...cartItems];
updatedCartItems[existingItemIndex] = {
...updatedCartItems[existingItemIndex],
quantity: updatedCartItems[existingItemIndex].quantity + 1,
};
// console.log('cart',cartItems.length);
// setTotalPrice(prev=>prev-dish.price)
setCartItems(updatedCartItems);
} else {
// If the dish is not in the cart, add it
console.log( "Dish does not exist in the cart. Adding to the cart." );
console.log( "cart" , cartItems.length);
// setTotalPrice(prev=>prev-dish.price)
setCartItems([...cartItems, { ...dish, quantity: 1 }]);
}
setTotalPrice((prev) => prev + dish.price);
};
const handleRemoveItems = (dish) => {
console.log( "Dish ID to remove:" , dish);
// Check if the dish exists in the cart
const existingItemIndex = cartItems.findIndex(
(item) => item._id === dish._id
);
if (existingItemIndex !== -1) {
// If the dish exists, decrement the quantity
// or remove it from the cart
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) {
// If the quantity is greater than 1, decrement the quantity
updatedCartItems[existingItemIndex] = {
...updatedCartItems[existingItemIndex],
quantity: updatedCartItems[existingItemIndex].quantity - 1,
};
} else {
// If the quantity is 1, remove the dish from the cart
updatedCartItems.splice(existingItemIndex, 1);
}
setCartItems(updatedCartItems);
setTotalPrice((prev) => prev - dish.price);
} else {
// If the dish is not in the cart,
// log a message or handle accordingly
console.log( "Dish does not exist in the cart." );
}
};
const value = {
restaurants,
selectedRestaurant,
setSelectedRestaurant,
handleAddItems,
handleRemoveItems,
totalPrice,
};
return (
<RestaurantContext.Provider value={value}>
{children}
</RestaurantContext.Provider>
);
}; export { RestaurantContext, RestaurantProvider }; |
//components/RestaurantList.js import React, { useContext, useState, useEffect } from "react" ;
import RestaurantCard from "./RestaurantCard" ;
import { RestaurantContext } from "../contexts/RestaurantContext" ;
const RestaurantList = () => { const { restaurants, setSelectedRestaurant } =
useContext(RestaurantContext);
const [filteredRestaurants, setFilteredRestaurants] = useState([
...restaurants,
]);
const [ratingFilter, setRatingFilter] = useState( "" );
const [searchTerm, setSearchTerm] = useState( "" );
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);
};
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"
/>
</div>
<div className= "restaurant-card-container" >
{filteredRestaurants.map((restaurant) => (
<RestaurantCard
key={restaurant._id}
restaurant={restaurant}
onClick={() => handleRestaurantClick(restaurant._id)}
/>
))}
</div>
</div>
);
}; export default RestaurantList;
|
//components/RestaurantCard.js 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;
|
//components/DishesMenu.js 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;
)}
</div>
);
}; export default DishesMenu;
|
//components/DishCard.js 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;
|
//components/Cart.js import React, { useContext } from 'react' ;
import { RestaurantContext } from '../contexts/RestaurantContext' ;
const Cart = () => { const { totalPrice } = useContext(RestaurantContext);
return (
<div className= "cart-container" >
<h2>Cart</h2>
<div className= "cart-content" >
<span style={{ color: "brown" }}>Total Price: </span> ${totalPrice}
{ /* Add other cart items here */ }
</div>
</div>
);
}; export default Cart;
|
/* App.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 ;
} /* Styles to Resturant List */ /* RestaurantList.css */ .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 : #4caf50 ;
color : white ;
padding : 10px ;
} .filter-container { display : flex;
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 ;
} /* RestaurantCard.css */ .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 ;
/* Set a fixed height for the image container */
} .restaurant-image { width : 100% ;
height : 100% ;
object-fit: cover;
/* Maintain the aspect ratio and cover the container */
border-radius: 8px ;
} /* Responsive Styles */ @media screen and ( max-width : 600px ) {
.card {
width : 100% ;
}
} /* Dish Card */ .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% ;
/* Set the width to fill the container */
height : 100px ;
/* Set the fixed height for the image */
object-fit: cover;
/* Maintain the aspect ratio and cover the container */
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 ;
} /* Responsive Styles */ @media screen and ( max-width : 600px ) {
.dish-card {
width : 100% ;
}
} /* CART */ .cart-container { position : fixed ;
top : 10px ;
right : 10px ;
width : 200px ;
border : 2px solid #4CAF50 ;
border-radius: 10px ;
height : 20 vh;
background-color : #fff ;
box-shadow: 0 0 10px rgba( 0 , 0 , 0 , 0.1 );
overflow-y: auto ;
z-index : 1000 ;
} h 2 {
margin-bottom : 10px ;
margin-left : 20px ;
} .cart-content { padding : 16px ;
} |
Step to Run the code by typing the following command
- Start the backend:
node server.js
- Start the frontend:
npm start
Output: