DEV Community

habeebahmed
habeebahmed

Posted on

Tutorial on developing Yelp Clone

Disclaimer: The code was written with the help of ChatGPT

Features that will be covered in this blog are:

  1. Login and Signup.
  2. View restaurant ratings.
  3. Provide feedback and ratings to restaurants.
  4. Upload photos.

Tech stack: React JS, Node JS, MongoDB, AWS and EKS

We shall be going through following steps:

1. Set up the development environment:

  • Install Node.js and npm (Node Package Manager) if you haven't already.
  • Install MongoDB locally or set up a cloud instance using MongoDB Atlas.
  • Install the AWS CLI and configure your AWS account credentials.

2. Initialize the project:

  • Create a new directory for your project and navigate to it in the terminal.
  • Run npm init to create a package.json file for your project.
  • Run npm install react react-dom to install React and ReactDOM.
  • Run npm install express mongoose to install Express.js (Node.js web framework) and Mongoose (MongoDB object modeling tool).

3. Set up the backend:

  • Create a new directory named server in your project root.
  • Inside the server directory, create a new file named app.js. - This file will contain the server logic.
  • Set up the Express.js server, connect to the MongoDB database using Mongoose, and define the required routes (e.g., for user authentication, fetching restaurant ratings, and adding new ratings and photos).

4. Set up the frontend:

  • Create a new directory named client in your project root.
  • Inside the client directory, create a new directory named src. This directory will contain your React components and application logic.
  • Create a new file named index.js in the src directory. This file will render the root React component and mount it to the DOM.
  • Create React components for the login/signup page, the restaurant ratings display, and the rating/review submission form.

5. Implement user authentication:

  • In the backend, set up user registration and login routes using Passport.js and JSON Web Tokens (JWT) for authentication.
  • In the frontend, create a form for users to log in or sign up, and store the JWT in the browser's local storage.

6. Implement the restaurant ratings feature:

  • Create a MongoDB schema for restaurants, which includes fields such as the name, location, average rating, and user reviews.
  • In the backend, create API routes to fetch and submit restaurant ratings.
  • In the frontend, create components to display the restaurant ratings and submit new ratings after a user logs in.

7. Implement the photo upload feature:

  • Use AWS S3 to store user-uploaded photos.
  • In the backend, create an API route to handle file uploads, using the multer middleware to handle file uploads in Express.js, and the aws-sdk to interact with AWS S3.
  • In the frontend, add a file input field to the review submission form, and modify the form submission logic to include the photo upload.

8. Set up multi-environment configurations:

  • Create separate configuration files (e.g., .env.development, .env.production) to store environment-specific variables, such as API endpoints and MongoDB connection strings.
  • Use the dotenv package to load the appropriate configuration file based on the current environment.

9. Deploy the application:

  • Set up AWS ECS (Elastic Container Service) and EKS (Elastic Kubernetes Service) to deploy your application on a container orchestration platform.
  • Create a Dockerfile for your application, and build a Docker image.
  • Push the Docker image to Amazon ECR (Elastic Container Registry).
  • Create a Kubernetes deployment and service manifest for your application.
  • Use kubectl to apply the manifest and deploy your application on EKS.

Project Structure:

backend/
  |- models/
  |   |- User.js
  |   |- Restaurant.js
  |   └- Review.js
  |- routes/
  |   |- auth.js
  |   |- restaurant.js
  |   └- review.js
  |- middleware/
  |   |- auth.js
  |   └- errorHandler.js
  |- utils/
  |   └- upload.js
  |- .env
  |- app.js
  └- package.json
Enter fullscreen mode Exit fullscreen mode
frontend/
  |- public/
  |   └- index.html
  |- src/
  |   |- components/
  |   |   |- Navbar.js
  |   |   |- RestaurantCard.js
  |   |   |- ReviewForm.js
  |   |   └- PhotoUploader.js
  |   |- contexts/
  |   |   └- AuthContext.js
  |   |- pages/
  |   |   |- HomePage.js
  |   |   |- LoginPage.js
  |   |   |- SignupPage.js
  |   |   |- UserProfile.js
  |   |   └- RestaurantDetails.js
  |   |- utils/
  |   |   └- api.js
  |   |- App.js
  |   |- Router.js
  |   └- index.js
  └- package.json
Enter fullscreen mode Exit fullscreen mode

Backend Setup

  • package.json
{
  "name": "backend",
  "version": "1.0.0",
  "description": "Backend for the Yelp-like web application",
  "main": "app.js",
  "scripts": {
    "start": "node app.js",
    "dev": "nodemon app.js"
  },
  "repository": {
    "type": "git",
    "url": "your-repository-url"
  },
  "author": "Your Name",
  "license": "MIT",
  "dependencies": {
    "bcrypt": "^5.0.1",
    "cors": "^2.8.5",
    "dotenv": "^10.0.0",
    "express": "^4.17.1",
    "jsonwebtoken": "^8.5.1",
    "mongoose": "^6.1.2",
    "multer": "^1.4.4"
  },
  "devDependencies": {
    "nodemon": "^2.0.15"
  }
}

Enter fullscreen mode Exit fullscreen mode
  • .env
MONGO_URI=<your_mongodb_connection_uri>
JWT_SECRET=<your_jwt_secret_key>
PORT=5000
Enter fullscreen mode Exit fullscreen mode

Replace with your MongoDB connection URI and with a secure and unique secret key for your JSON Web Token (JWT) authentication.

  • server/app.js(Express.js server setup and MongoDB connection)
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
const dotenv = require('dotenv');
const errorHandler = require('./middleware/errorHandler');

// Load environment variables
dotenv.config();

const app = express();

// Middleware
app.use(cors());
app.use(express.json());

// Connect to MongoDB
mongoose.connect(process.env.MONGODB_URI, {
  useNewUrlParser: true,
  useUnifiedTopology: true,
})
.then(() => console.log('Connected to MongoDB'))
.catch((err) => console.error('Failed to connect to MongoDB', err));

// Import and use API routes
const authRoutes = require('./routes/auth');
const restaurantRoutes = require('./routes/restaurant');

app.use('/api/auth', authRoutes);
app.use('/api/restaurants', restaurantRoutes);

// Add the custom error handling middleware
app.use(errorHandler);

// Start the server
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));

Enter fullscreen mode Exit fullscreen mode
  • Models #### Backend - server/models/User.js (User schema for authentication)
const mongoose = require('mongoose');
const bcrypt = require('bcrypt');

const userSchema = new mongoose.Schema({
  email: {
    type: String,
    required: true,
    unique: true,
  },
  password: {
    type: String,
    required: true,
  },
});

// Hash the password before saving the user
userSchema.pre('save', async function (next) {
  if (this.isModified('password')) {
    this.password = await bcrypt.hash(this.password, 10);
  }
  next();
});

// Compare password hashes for authentication
userSchema.methods.comparePassword = function (candidatePassword) {
  return bcrypt.compare(candidatePassword, this.password);
};

module.exports = mongoose.model('User', userSchema);

Enter fullscreen mode Exit fullscreen mode

Backend - server/models/Restaurants.js

const mongoose = require('mongoose');

const RestaurantSchema = new mongoose.Schema({
  name: {
    type: String,
    required: true
  },
  address: {
    type: String,
    required: true
  },
  rating: {
    type: Number,
    default: 0
  },
  reviews: [
    {
      type: mongoose.Schema.Types.ObjectId,
      ref: 'Review'
    }
  ],
  createdAt: {
    type: Date,
    default: Date.now
  }
});

module.exports = mongoose.model('Restaurant', RestaurantSchema);

Enter fullscreen mode Exit fullscreen mode

Backend - server/models/Review.js

const mongoose = require('mongoose');

const ReviewSchema = new mongoose.Schema({
  user: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'User',
    required: true
  },
  restaurant: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'Restaurant',
    required: true
  },
  text: {
    type: String,
    required: true
  },
  rating: {
    type: Number,
    required: true
  },
  photos: [
    {
      url: {
        type: String,
        required: true
      }
    }
  ],
  createdAt: {
    type: Date,
    default: Date.now
  }
});

module.exports = mongoose.model('Review', ReviewSchema);

Enter fullscreen mode Exit fullscreen mode
  • Routes #### Backend - server/routes/auth.js (User authentication routes)
const express = require('express');
const jwt = require('jsonwebtoken');
const User = require('../models/User');

const router = express.Router();

// POST /api/auth/signup - register a new user
router.post('/signup', async (req, res) => {
  const { email, password } = req.body;

  try {
    const user = new User({ email, password });
    await user.save();
    res.status(201).json({ message: 'User created' });
  } catch (error) {
    res.status(500).json({ message: 'Error creating user' });
  }
});

// POST /api/auth/login - log in a user and return a JWT
router.post('/login', async (req, res) => {
  const { email, password } = req.body;

  try {
    const user = await User.findOne({ email });

    if (!user || !(await user.comparePassword(password))) {
      return res.status(401).json({ message: 'Invalid email or password' });
    }

    const token = jwt.sign({ userId: user._id }, process.env.JWT_SECRET, { expiresIn: '1d' });
    res.status(200).json({ token });
  } catch (error) {
    res.status(500).json({ message: 'Error logging in user' });
  }
});

module.exports = router;

Enter fullscreen mode Exit fullscreen mode

Backend - server/routes/restaurant.js (API route to fetch a restaurant's details and its reviews)

const express = require('express');
const Restaurant = require('../models/Restaurant');
const Review = require('../models/Review');
const auth = require('../middleware/auth');

const router = express.Router();

// GET /api/restaurants/:id - fetch a restaurant's details and its reviews
router.get('/:id', async (req, res) => {
  try {
    const restaurant = await Restaurant.findById(req.params.id);
    if (!restaurant) {
      return res.status(404).json({ message: 'Restaurant not found' });
    }

    const reviews = await Review.find({ restaurant: req.params.id }).populate('user', 'email');

    res.json({ restaurant, reviews });
  } catch (error) {
    res.status(500).json({ message: 'Error fetching restaurant details' });
  }
});

router.get('/', async (req, res) => {
  try {
    const restaurants = await Restaurant.find();
    res.json(restaurants);
  } catch (error) {
    res.status(500).json({ message: 'Error fetching restaurants' });
  }
});

// POST /api/restaurants - create a new restaurant (requires authentication)
router.post('/', auth, async (req, res) => {
  const { name, description } = req.body;

  try {
    const restaurant = new Restaurant({
      name,
      description,
    });

    await restaurant.save();
    res.status(201).json({ message: 'Restaurant created' });
  } catch (error) {
    res.status(500).json({ message: 'Error creating restaurant' });
  }
});

module.exports = router;
Enter fullscreen mode Exit fullscreen mode

Backend - server/routes/review.js (API route to submit a new review)

const express = require('express');
const auth = require('../middleware/auth');
const Review = require('../models/Review');

const router = express.Router();

// POST /api/reviews - submit a new review (requires authentication)
router.post('/', auth, async (req, res) => {
  const { restaurantId, rating, comment } = req.body;

  try {
    const review = new Review({
      user: req.userId,
      restaurant: restaurantId,
      rating,
      comment,
    });

    await review.save();
    res.status(201).json({ message: 'Review submitted' });
  } catch (error) {
    res.status(500).json({ message: 'Error submitting review' });
  }
});

module.exports = router;
Enter fullscreen mode Exit fullscreen mode

Backend - server/middleware/auth.js (JWT authentication middleware)

const jwt = require('jsonwebtoken');

// Middleware to verify the JWT and extract the user ID
const auth = (req, res, next) => {
  const token = req.header('Authorization');

  if (!token) {
    return res.status(401).json({ message: 'No token provided' });
  }

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.userId = decoded.userId;
    next();
  } catch (error) {
    res.status(401).json({ message: 'Invalid token' });
  }
};

module.exports = auth;
Enter fullscreen mode Exit fullscreen mode

Backend - server/middleware/errorHandler.js (JWT authentication middleware)

const errorHandler = (err, req, res, next) => {
  // Log the error for debugging purposes
  console.error(err.stack);

  // Set default error properties
  let statusCode = 500;
  let message = 'An unexpected error occurred';

  // Customize the error response based on the error type
  if (err.name === 'ValidationError') {
    statusCode = 400;
    message = err.message;
  } else if (err.name === 'CastError') {
    statusCode = 400;
    message = 'Invalid ID format';
  } else if (err.name === 'UnauthorizedError') {
    statusCode = 401;
    message = 'Unauthorized';
  }

  // Send the error response
  res.status(statusCode).json({
    error: {
      message: message,
      status: statusCode,
      details: process.env.NODE_ENV === 'development' ? err.stack : undefined
    }
  });
};

module.exports = errorHandler;
Enter fullscreen mode Exit fullscreen mode

Backend - server/utils/upload.js (JWT authentication middleware)

const multer = require('multer');
const path = require('path');

// Configure storage for file uploads
const storage = multer.diskStorage({
  destination: function (req, file, cb) {
    cb(null, 'uploads/');
  },
  filename: function (req, file, cb) {
    const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
    cb(null, uniqueSuffix + path.extname(file.originalname));
  }
});

// File upload filter to check for valid file types
const fileFilter = (req, file, cb) => {
  const filetypes = /jpeg|jpg|png/;
  const mimetype = filetypes.test(file.mimetype);
  const extname = filetypes.test(path.extname(file.originalname).toLowerCase());

  if (mimetype && extname) {
    return cb(null, true);
  }
  cb(new Error('Only jpeg, jpg, and png images are allowed.'));
};

// Initialize multer with the defined storage and filter options
const upload = multer({
  storage: storage,
  fileFilter: fileFilter,
  limits: {
    fileSize: 5 * 1024 * 1024 // Limit file size to 5MB
  }
});

module.exports = upload;
Enter fullscreen mode Exit fullscreen mode

Frontend setup

Frontend - public/index.html

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="utf-8" />
  <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <meta name="theme-color" content="#000000" />
  <meta name="description" content="A Yelp-like web application" />
  <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
  <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
  <title>Yelp-like Web Application</title>
</head>

<body>
  <noscript>You need to enable JavaScript to run this app.</noscript>
  <div id="root"></div>
  <!--
    This HTML file is a template.
    If you open it directly in the browser, you will see an empty page.

    You can add webfonts, meta tags, or analytics to this file.
    The build step will place the bundled scripts into the <body> tag.

    To begin the development, run `npm start` or `yarn start`.
    To create a production bundle, use `npm run build` or `yarn build`.
  -->
</body>

</html>

Enter fullscreen mode Exit fullscreen mode

Frontend - src/components/Navbar.js

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

const Navbar = () => {
  return (
    <nav className="navbar">
      <div className="container">
        <Link to="/" className="navbar-brand">
          Yelp-like App
        </Link>
        <ul className="navbar-nav">
          <li className="nav-item">
            <Link to="/restaurants" className="nav-link">
              Restaurants
            </Link>
          </li>
          <li className="nav-item">
            <Link to="/login" className="nav-link">
              Login
            </Link>
          </li>
          <li className="nav-item">
            <Link to="/signup" className="nav-link">
              Sign Up
            </Link>
          </li>
        </ul>
      </div>
    </nav>
  );
};

export default Navbar;

Enter fullscreen mode Exit fullscreen mode

Frontend - src/components/RestaurantCard.js

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

const RestaurantCard = ({ restaurant }) => {
  return (
    <div className="restaurant-card">
      <img src={restaurant.image} alt={restaurant.name} className="restaurant-image" />
      <div className="restaurant-details">
        <h3 className="restaurant-name">
          <Link to={`/restaurants/${restaurant._id}`}>{restaurant.name}</Link>
        </h3>
        <p className="restaurant-address">{restaurant.address}</p>
        <div className="restaurant-rating">
          <span>{restaurant.rating.toFixed(1)}</span>
          <span className="rating-stars">{/* Add star rating component here */}</span>
        </div>
        <p className="restaurant-review-count">{restaurant.reviews.length} reviews</p>
      </div>
    </div>
  );
};

export default RestaurantCard;

Enter fullscreen mode Exit fullscreen mode

Frontend - src/components/ReviewForm.js

import React, { useState } from 'react';

const ReviewForm = ({ onSubmit }) => {
  const [text, setText] = useState('');
  const [rating, setRating] = useState(1);

  const handleSubmit = (e) => {
    e.preventDefault();
    onSubmit({ text, rating });
    setText('');
    setRating(1);
  };

  return (
    <form className="review-form" onSubmit={handleSubmit}>
      <div className="form-group">
        <label htmlFor="text">Review:</label>
        <textarea
          id="text"
          name="text"
          value={text}
          onChange={(e) => setText(e.target.value)}
          required
        ></textarea>
      </div>
      <div className="form-group">
        <label htmlFor="rating">Rating:</label>
        <select
          id="rating"
          name="rating"
          value={rating}
          onChange={(e) => setRating(parseInt(e.target.value))}
        >
          <option value="1">1</option>
          <option value="2">2</option>
          <option value="3">3</option>
          <option value="4">4</option>
          <option value="5">5</option>
        </select>
      </div>
      <button type="submit" className="submit-review">
        Submit Review
      </button>
    </form>
  );
};

export default ReviewForm;
Enter fullscreen mode Exit fullscreen mode

Frontend - src/components/PhotoUploader.js

import React, { useState } from 'react';

const PhotoUploader = ({ onUpload }) => {
  const [selectedFile, setSelectedFile] = useState(null);

  const handleFileChange = (e) => {
    setSelectedFile(e.target.files[0]);
  };

  const handleSubmit = async (e) => {
    e.preventDefault();

    if (!selectedFile) {
      alert('Please select a file to upload.');
      return;
    }

    const formData = new FormData();
    formData.append('image', selectedFile);

    // Call the onUpload function with the formData
    onUpload(formData);

    // Reset the file input
    setSelectedFile(null);
    e.target.reset();
  };

  return (
    <form className="photo-uploader" onSubmit={handleSubmit}>
      <div className="form-group">
        <input type="file" accept="image/*" onChange={handleFileChange} />
      </div>
      <button type="submit" className="upload-button">
        Upload Photo
      </button>
    </form>
  );
};

export default PhotoUploader;

Enter fullscreen mode Exit fullscreen mode

Frontend - src/contexts/AuthContext.js

import React, { createContext, useState, useEffect } from 'react';

export const AuthContext = createContext();

const AuthProvider = ({ children }) => {
  const [currentUser, setCurrentUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // Simulate an asynchronous function call to check for an authenticated user
    const fetchCurrentUser = async () => {
      // Replace this with a call to your backend to fetch the current user
      const user = await new Promise((resolve) => {
        setTimeout(() => resolve(null), 1000);
      });

      setCurrentUser(user);
      setLoading(false);
    };

    fetchCurrentUser();
  }, []);

  const login = async (email, password) => {
    // Perform authentication and set the currentUser
    // Replace this with a call to your backend to login the user
  };

  const signup = async (email, password) => {
    // Register a new user and set the currentUser
    // Replace this with a call to your backend to register the user
  };

  const logout = async () => {
    // Log out the user and reset the currentUser
    // Replace this with a call to your backend to log out the user
    setCurrentUser(null);
  };

  const value = {
    currentUser,
    loading,
    login,
    signup,
    logout,
  };

  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};

export default AuthProvider;

Enter fullscreen mode Exit fullscreen mode

Frontend - src/pages/HomePage.js

import React, { useEffect, useState } from 'react';

const HomePage = () => {
  const [restaurants, setRestaurants] = useState([]);

  useEffect(() => {
    fetch('/api/restaurants')
      .then((response) => response.json())
      .then((data) => setRestaurants(data))
      .catch((error) => console.error('Error fetching restaurant ratings', error));
  }, []);

  return (
    <div>
      <h1>Restaurant Ratings</h1>
      <ul>
        {restaurants.map((restaurant) => (
          <li key={restaurant._id}>
            <h2>{restaurant.name}</h2>
            <p>Location: {restaurant.location}</p>
            <p>Rating: {restaurant.rating}</p>
          </li>
        ))}
      </ul>
    </div>
  );
};

export default HomePage;
Enter fullscreen mode Exit fullscreen mode

Frontend - src/pages/LoginPage.js

import React, { useState } from 'react';

const LoginPage = () => {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const handleSubmit = async (e) => {
    e.preventDefault();

    try {
      const response = await fetch('/api/auth/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email, password }),
      });

      if (!response.ok) {
        throw new Error('Invalid email or password');
      }

      const data = await response.json();
      localStorage.setItem('token', data.token);
      alert('Logged in successfully');
    } catch (error) {
      alert(error.message);
    }
  };

  return (
    <div>
      <h1>Login</h1>
      <form onSubmit={handleSubmit}>
        <label>
          Email:
          <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
        </label>
        <label>
          Password:
          <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
        </label>


<button type="submit">Login</button>
  </form>
</div>
);
};

export default LoginPage;
Enter fullscreen mode Exit fullscreen mode

Frontend - src/pages/SignupPage.js

import React, { useState } from 'react';

const SignupPage = () => {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const handleSubmit = async (e) => {
    e.preventDefault();

    try {
      const response = await fetch('/api/auth/signup', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email, password }),
      });

      if (!response.ok) {
        throw new Error('Error creating user');
      }

      alert('User created successfully');
    } catch (error) {
      alert(error.message);
    }
  };

  return (
    <div>
      <h1>Sign Up</h1>
      <form onSubmit={handleSubmit}>
        <label>
          Email:
          <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
        </label>
        <label>
          Password:
          <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
        </label>
        <button type="submit">Sign Up</button>
      </form>
    </div>
  );
};

export default SignupPage;
Enter fullscreen mode Exit fullscreen mode

Frontend - src/pages/UserProfile.js

import React, { useContext } from 'react';
import { AuthContext } from './AuthContext';

const UserProfile = () => {
  const { currentUser, logout } = useContext(AuthContext);

  if (!currentUser) {
    return <p>Please log in to view your profile.</p>;
  }

  return (
    <div className="user-profile">
      <h2>User Profile</h2>
      <p>Email: {currentUser.email}</p>
      <p>Name: {currentUser.name}</p>
      {/* Add more user details here */}

      <button onClick={logout}>Log Out</button>
    </div>
  );
};

export default UserProfile;

Enter fullscreen mode Exit fullscreen mode

Frontend - src/pages/RestaurantDetails.js

import React, { useState, useEffect } from 'react';
import ReviewForm from '../components/ReviewForm';

const RestaurantDetails = ({ match }) => {
  const [restaurant, setRestaurant] = useState(null);
  const [reviews, setReviews] = useState([]);

  useEffect(() => {
    const fetchRestaurant = async () => {
      const response = await fetch(`/api/restaurants/${match.params.id}`);
      const data = await response.json();
      setRestaurant(data.restaurant);
      setReviews(data.reviews);
    };

    fetchRestaurant();
  }, [match.params.id]);

  if (!restaurant) {
    return <div>Loading...</div>;
  }

  return (
    <div>
      <h1>{restaurant.name}</h1>
      <p>{restaurant.description}</p>
      <h2>Reviews</h2>
      {reviews.map((review) => (
        <div key={review._id}>
          <p>
            <strong>{review.user.email}</strong> - {review.rating} stars
          </p>
          <p>{review.comment}</p>
        </div>
      ))}
      <ReviewForm restaurantId={restaurant._id} />
    </div>
  );
};

export default RestaurantDetails;

Enter fullscreen mode Exit fullscreen mode

Frontend - src/utils/api.js

const BASE_URL = 'http://localhost:5000/api';

const headers = {
  'Content-Type': 'application/json',
};

const handleResponse = async (response) => {
  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.message || 'Something went wrong');
  }
  return response.json();
};

const api = {
  get: async (endpoint) => {
    const response = await fetch(`${BASE_URL}${endpoint}`, {
      method: 'GET',
      headers,
    });
    return handleResponse(response);
  },

  post: async (endpoint, data) => {
    const response = await fetch(`${BASE_URL}${endpoint}`, {
      method: 'POST',
      headers,
      body: JSON.stringify(data),
    });
    return handleResponse(response);
  },

  put: async (endpoint, data) => {
    const response = await fetch(`${BASE_URL}${endpoint}`, {
      method: 'PUT',
      headers,
      body: JSON.stringify(data),
    });
    return handleResponse(response);
  },

  delete: async (endpoint) => {
    const response = await fetch(`${BASE_URL}${endpoint}`, {
      method: 'DELETE',
      headers,
    });
    return handleResponse(response);
  },

  // Add any additional methods you may need, such as PATCH or custom request configurations
};

export default api;

Enter fullscreen mode Exit fullscreen mode

Frontend - src/App.js

import React from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import './App.css';
import Navbar from './components/Navbar';
import HomePage from './components/HomePage';
import RestaurantPage from './components/RestaurantPage';
import UserProfile from './components/UserProfile';
import Login from './components/Login';
import Signup from './components/Signup';

function App() {
  return (
    <Router>
      <div className="App">
        <Navbar />
        <div className="container">
          <Switch>
            <Route path="/" component={HomePage} exact />
            <Route path="/restaurants/:id" component={RestaurantPage} />
            <Route path="/profile" component={UserProfile} />
            <Route path="/login" component={Login} />
            <Route path="/signup" component={Signup} />
            {/* Add additional routes as needed */}
          </Switch>
        </div>
      </div>
    </Router>
  );
}

export default App;

Enter fullscreen mode Exit fullscreen mode

Frontend - src/Router.js

import React from 'react';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import HomePage from './pages/HomePage';
import LoginPage from './pages/LoginPage';
import SignupPage from './pages/SignupPage';

const Router = () => {
  return (
    <BrowserRouter>
      <Switch>
        <Route path="/" exact component={HomePage} />
        <Route path="/login" component={LoginPage} />
        <Route path="/signup" component={SignupPage} />
      </Switch>
    </BrowserRouter>
  );
};

export default Router;
Enter fullscreen mode Exit fullscreen mode

Frontend - src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import AuthProvider from './AuthContext';
import reportWebVitals from './reportWebVitals';

ReactDOM.render(
  <React.StrictMode>
    <AuthProvider>
      <App />
    </AuthProvider>
  </React.StrictMode>,
  document.getElementById('root')
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
Enter fullscreen mode Exit fullscreen mode

frontend/package.json

{
  "name": "frontend",
  "version": "1.0.0",
  "private": true,
  "dependencies": {
    "@testing-library/jest-dom": "^5.16.1",
    "@testing-library/react": "^12.1.3",
    "@testing-library/user-event": "^13.5.0",
    "axios": "^0.26.1",
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "react-router-dom": "^6.3.6",
    "react-scripts": "5.0.0",
    "web-vitals": "^2.1.4"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest"
    ]
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Deployment strategy:

1. Set up an Amazon EKS cluster:
- Install and configure the eksctl command line tool.
- Create an EKS cluster using the eksctl create cluster command.
- Configure kubectl to communicate with your cluster.

2. Create Kubernetes manifests for your frontend and backend services. You will need to create Deployment and Service manifests for both frontend and backend. Make sure to update the image names in the manifests with the ones you pushed to Docker Hub.

3. Apply the Kubernetes manifests using kubectl apply -f <manifest-file>.

- Access your application using the Kubernetes LoadBalancer service's external IP.
Enter fullscreen mode Exit fullscreen mode
  • Set up an Amazon EKS cluster:
  • Install and configure the aws CLI and eksctl command-line tool.
  • Create a new EKS cluster using the eksctl create cluster command. Refer to the official documentation for more details. Configure kubectl to communicate with your cluster: Update the kubeconfig for your cluster using the following command:
aws eks update-kubeconfig --region your-region --name your-cluster-name
Enter fullscreen mode Exit fullscreen mode
  • Create Kubernetes manifests for your frontend and backend services: -- Create a k8s directory in your project root and store the following manifests:

-- Frontend Deployment: frontend-deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend
spec:
  replicas: 1
  selector:
    matchLabels:
      app: frontend
  template:
    metadata:
      labels:
        app: frontend
    spec:
      containers:
      - name: frontend
        image: your-dockerhub-username/frontend
        ports:
        - containerPort: 80

Enter fullscreen mode Exit fullscreen mode

-- Frontend Service: frontend-service.yaml

apiVersion: v1
kind: Service
metadata:
  name: frontend
spec:
  selector:
    app: frontend
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
  type: LoadBalancer
Enter fullscreen mode Exit fullscreen mode

-- Backend Deployment: backend-deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend
spec:
  replicas: 1
  selector:
    matchLabels:
      app: backend
  template:
    metadata:
      labels:
        app: backend
    spec:
      containers:
      - name: backend
        image: your-dockerhub-username/backend
        ports:
        - containerPort: 5000
        env:
        - name: MONGO_URI
          value: your-mongodb-atlas-uri
        - name: JWT_SECRET
          value: your-jwt-secret

Enter fullscreen mode Exit fullscreen mode

-- Backend Service: backend-service.yaml

apiVersion: v1
kind: Service
metadata:
  name: backend
spec:
  selector:
    app: backend
  ports:
    - protocol: TCP
      port: 5000
      targetPort: 5000
  type: LoadBalancer

Enter fullscreen mode Exit fullscreen mode
  • Apply the Kubernetes manifests: Deploy your application using the following commands:
kubectl apply -f k8s/frontend-deployment.yaml
kubectl apply -f k8s/frontend-service.yaml
kubectl apply -f k8s/backend-deployment.yaml
kubectl apply -f k8s/backend-service.yaml
Enter fullscreen mode Exit fullscreen mode
  • Access your application: -- Get the external IP addresses for the frontend and backend services:

kubectl get svc

Top comments (1)

Collapse
 
victoria_mostova profile image
Victoria Mostova

Hello Habeeb!

You've created a very informative guide for anyone looking to make an app like Yelp. One of the things that I appreciated about this tutorial is that it focuses on building the core features of Yelp, such as user authentication, business listings, and user reviews. This makes it a great starting point for anyone looking to develop an app like Yelp, as it provides a solid foundation for building more advanced features in the future.

Although this tutorial is focused on building a Yelp clone using Ruby on Rails and PostgreSQL, the principles and techniques covered can be applied to other web development frameworks and technologies.