Deploying Full-Stack MERN Application on an NGINX VPS

Deploying Full-Stack MERN Application on an NGINX VPS

Creating a Full-Stack MERN Application and Deploying it on an NGINX VPS

Overview of MERN Stack

MERN stands for MongoDB, Express, React, and Node.js. MongoDB is a NoSQL database, Express is a web application framework for Node.js, React is a JavaScript library for building user interfaces, and Node.js is a JavaScript runtime built on Chrome’s V8 JavaScript engine.

Prerequisites

  • Node.js installed on your local machine
  • A code editor (e.g., VS Code)
  • A VPS with root access for deployment (e.g., DigitalOcean, AWS, or Linode)
  • Basic knowledge of JavaScript, React, Node.js, and MongoDB

Project Structure

mern-app/
├── backend/
│   ├── config/
│   ├── controllers/
│   ├── middleware/
│   ├── models/
│   ├── routes/
│   ├── app.js
│   └── package.json
├── frontend/
│   ├── public/
│   ├── src/
│   │   ├── components/
│   │   ├── api/
│   │   ├── App.js
│   │   └── index.js
│   └── package.json
└── nginx/
    └── mern-app.conf

Create the Backend

Step 1: Create a new directory called mern-app and navigate into it:

mkdir mern-app
cd mern-app

Step 2: Create a new folder called backend:

mkdir backend
cd backend

Step 3: Initialize the project and install required packages:

npm init -y
npm install express mongoose dotenv cors

Step 4: Create required folders and files:

mkdir config controllers middleware models routes
touch app.js

Step 5: Configure the app and create a simple REST API:

  • config/db.js: Connect to MongoDB
  • models/User.js: Create a User model
  • routes/userRoutes.js: Create user routes
  • controllers/userController.js: Implement user controllers
  • middleware/auth.js: Implement authentication middleware
  • app.js: Set up the Express server

Create the Frontend

Step 1: Navigate to the mern-app folder and create a new folder called frontend:

cd ..
mkdir frontend
cd frontend

Step 2: Use create-react-app to initialize the project:

npx create-react-app .

Step 3: Install required packages:

npm install axios react-router-dom

Step 4: Create required folders and files:

mkdir src/components src/api

Step 5: Configure the app and create the frontend:

  • src/components/: Create required components (e.g., Header, LoginForm, etc.)
  • src/api/: Implement API calls to the backend
  • src/App.js: Set up React Router and main app layout
  • src/index.js: Render the main app component
  1. Connect Frontend and Backend
  • Use Axios in the frontend/src/api/ files to make API calls to the backend
  • Enable CORS in the backend/app.js to allow cross-origin requests

Production-Ready Configuration

  • Use environment variables (.env files) for sensitive information
  • Implement proper error handling and logging
  • Use secure middleware like helmet and express-rate-limit for security
  • Enable gzip compression in the NGINX configuration
  • Optimize React build using npm run build

Deployment on NGINX VPS

Step 1: Set up your VPS

  • Connect to your VPS via SSH
  • Update packages and install required software:
sudo apt update
sudo apt upgrade
sudo apt install nginx git build-essential

Step 2: Install Node.js and MongoDB

  • Node.js:
curl -fsSL https://deb.nodesource.com/setup_16.x | sudo -E bash -
sudo apt-get install -y nodejs
  • MongoDB:

Follow the instructions for your specific VPS platform: https://docs.mongodb.com/manual/administration/install-on-linux/

Step 3: Clone your project

  • Clone your project to a directory, e.g., /var/www/mern-app:
sudo git clone <your-repo-url> /var/www/mern-app

Step 4: Install dependencies and build the frontend

  • Navigate to the backend and frontend directories and install dependencies:
cd /var/www/mern-app/backend
npm install

cd /var/www/mern-app/frontend
npm install
npm run build

Step 5: Set up environment variables for the backend

  • Create a .env file in the backend directory and fill it with the required variables (e.g., MONGO_URI, JWT_SECRET, PORT)

Step 6: Configure NGINX

  • Remove the default NGINX configuration:
sudo rm /etc/nginx/sites-enabled/default
  • Create a new NGINX configuration file for your app:
sudo nano /etc/nginx/sites-available/mern-app
  • Add the following configuration, replacing the server_name with your domain:
server {
    listen 80;
    server_name yourdomain.com;

    location / {
        root /var/www/mern-app/frontend/build;
        try_files $uri /index.html;
    }

    location /api {
        proxy_pass http://localhost:5000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }
}
  • Create a symbolic link to enable the configuration:
sudo ln -s /etc/nginx/sites-available/mern-app /etc/nginx/sites-enabled/
  • Test and reload the NGINX configuration:
sudo nginx -t
sudo systemctl reload nginx

Step 7: Set up a process manager for the backend

  • Install PM2:
sudo npm install -g pm2
  • Start the backend app:
cd /var/www/mern-app/backend
pm2 start app.js --name mern-app-backend
  • Configure PM2 to start on system boot:
pm2 startup
pm2 save

Step 8: Configure SSL (optional, but recommended)

Your full-stack MERN application should now be deployed and accessible via your domain. Remember to follow best practices for security, performance, and production readiness as mentioned earlier in the guide.

9. Backend and Frontend examples

Backend

  1. Set up Express server (backend/app.js):
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
const dotenv = require('dotenv');

dotenv.config();

const app = express();

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

const uri = process.env.MONGO_URI;

mongoose.connect(uri, { useNewUrlParser: true, useUnifiedTopology: true })
  .then(() => console.log('MongoDB Connected'))
  .catch(err => console.log(err));

const itemsRouter = require('./routes/items');

app.use('/items', itemsRouter);

const port = process.env.PORT || 5000;
app.listen(port, () => console.log(`Server running on port ${port}`));
  1. Create an Item model (backend/models/Item.js):
const mongoose = require('mongoose');

const itemSchema = new mongoose.Schema({
  name: {
    type: String,
    required: true
  }
});

module.exports = mongoose.model('Item', itemSchema);
  1. Create an items route (backend/routes/items.js):
const router = require('express').Router();
const Item = require('../models/Item');

router.get('/', async (req, res) => {
  try {
    const items = await Item.find();
    res.json(items);
  } catch (err) {
    res.status(500).json({ message: err.message });
  }
});

router.post('/', async (req, res) => {
  const newItem = new Item({ name: req.body.name });

  try {
    const savedItem = await newItem.save();
    res.status(201).json(savedItem);
  } catch (err) {
    res.status(400).json({ message: err.message });
  }
});

module.exports = router;

Frontend

  1. Set up React Router and main app layout (frontend/src/App.js):
import React from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import './App.css';
import ItemList from './components/ItemList';

function App() {
  return (
    <Router>
      <div className="App">
        <Switch>
          <Route path="/" exact component={ItemList} />
        </Switch>
      </div>
    </Router>
  );
}

export default App;
  1. Create a component for displaying and adding items to the list (frontend/src/components/ItemList.js):

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

const ItemList = () => {
  const [items, setItems] = useState([]);
  const [newItem, setNewItem] = useState('');

  useEffect(() => {
    const fetchItems = async () => {
      const res = await axios.get('/api/items');
      setItems(res.data);
    };

    fetchItems();
  }, []);

  const addItem = async (e) => {
    e.preventDefault();
    const res = await axios.post('/api/items', { name: newItem });
    setItems([...items, res.data]);
    setNewItem('');
  };

  return (
    <div>
      <h1>Item List</h1>
      <ul>
        {items.map((item) => (
          <li key={item._id}>{item.name}</li>
        ))}
      </ul>
      <form onSubmit={addItem}>
        <input
          type="text"
          value={newItem}
          onChange={(e) => setNewItem(e.target.value)}
        />
           <button type="submit">Add Item</button>
      </form>
   </div>
   );
};
export default ItemList;

3. Create an Axios instance to make API calls to the backend (frontend/src/api/index.js):

import axios from 'axios';

const instance = axios.create({
  baseURL: '/api',
});

export default instance;

4. Update the ItemList component to use the Axios instance:

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

const ItemList = () => {
  const [items, setItems] = useState([]);
  const [newItem, setNewItem] = useState('');

  useEffect(() => {
    const fetchItems = async () => {
      const res = await api.get('/items');
      setItems(res.data);
    };

    fetchItems();
  }, []);

  const addItem = async (e) => {
    e.preventDefault();
    const res = await api.post('/items', { name: newItem });
    setItems([...items, res.data]);
    setNewItem('');
  };

  return (
    <div>
      <h1>Item List</h1>
      <ul>
        {items.map((item) => (
          <li key={item._id}>{item.name}</li>
        ))}
      </ul>
      <form onSubmit={addItem}>
        <input
          type="text"
          value={newItem}
          onChange={(e) => setNewItem(e.target.value)}
        />
        <button type="submit">Add Item</button>
      </form>
    </div>
  );
};

export default ItemList;

With these code examples, the backend and frontend should now be functional. You can run both locally using npm start for the frontend and node app.js for the backend. Make sure to follow the previously provided steps to deploy the full-stack MERN application on an NGINX VPS.