React + Vite + Node.js + MongoDB CRUD Tutorial (2024)

Dedsec

Pedro Machado / December 28, 2024

11 min read •

Description

Learn how to build a simple CRUD application with React, Vite, Node.js, and MongoDB using modern best practices as of 2024.

Getting Started with React + Vite + Node.js + MongoDB: A Step-by-Step Tutorial

Welcome to this comprehensive guide on building a full-stack CRUD application with React, Vite, Node.js, and MongoDB. In this tutorial, we’ll cover everything from setting up your project to integrating the front-end and back-end, creating database models, implementing CRUD operations, and testing your application. By the end, you’ll have a solid understanding of how to build and deploy a modern, performant, and scalable CRUD application.

(Prefer watching? Check out my YouTube channel Pedrotechnologies for more tutorials and in-depth explanations!)


Table of Contents

  1. Introduction
  2. Setting Up the Environment
  3. Backend Configuration
  4. Frontend Configuration
  5. Implementing CRUD in React
  6. Testing the Application
  7. Deployment and Best Practices
  8. Conclusion

1. Introduction

CRUD (Create, Read, Update, Delete) operations lie at the heart of almost every web application. In this tutorial, we’ll use the latest tools and best practices of 2024 to build a simple but complete full-stack application. Our tech stack:

  • React (with Vite) for the front-end UI.
  • Node.js + Express for the back-end server.
  • MongoDB for the database.

Why This Stack?

  1. Scalability: Node.js excels at handling concurrent requests, making it a popular choice for modern web apps.
  2. Flexibility: React + Vite provides a fast, modular front-end environment with minimal configuration overhead.
  3. Performance: MongoDB’s document-based storage is both flexible and performant.
  4. Community and Ecosystem: Each component has a large ecosystem and active community, ensuring plenty of resources and support.

2. Setting Up the Environment

Prerequisites

Before proceeding, ensure you have the following installed on your machine:

  • Node.js v18+ (for the latest ES features and better performance)
  • npm, pnpm, or yarn (for installing dependencies)
  • MongoDB (locally or via a cloud provider like MongoDB Atlas)

Installing Required Tools

  1. Node.js and npm: Download and install from nodejs.org.
  2. pnpm (recommended): Installation guide.
  3. MongoDB:
    • Local: Install from mongodb.com.
    • Cloud (MongoDB Atlas): Sign up and create a cluster.

3. Backend Configuration

Initializing the Node.js + Express Project

  1. Create a new folder for your backend:

    mkdir server
    cd server
  2. Initialize a package.json file:

    npm init -y
    # or
    pnpm init
    # or
    yarn init -y
  3. Install Express and other dependencies:

    npm install express cors mongoose dotenv
    # or
    pnpm add express cors mongoose dotenv
    # or
    yarn add express cors mongoose dotenv
    • express: Popular Node.js framework for building APIs.
    • cors: Enables cross-origin resource sharing between front-end and back-end.
    • mongoose: ODM (Object Data Modeling) library for MongoDB.
    • dotenv: Loads environment variables from a .env file.
  4. Create a basic Express server:

    touch index.js

    index.js:

    require("dotenv").config();
    const express = require("express");
    const cors = require("cors");
     
    const app = express();
     
    app.use(cors());
    app.use(express.json()); // to parse JSON requests
     
    // Simple test endpoint
    app.get("/", (req, res) => {
      res.send("Server is running...");
    });
     
    const PORT = process.env.PORT || 4000;
    app.listen(PORT, () => {
      console.log(`Server listening on port ${PORT}`);
    });

Connecting to MongoDB

Inside index.js, we can connect to our MongoDB instance using Mongoose:

const mongoose = require("mongoose");
 
mongoose
  .connect(process.env.MONGO_URI, {
    useNewUrlParser: true,
    useUnifiedTopology: true,
  })
  .then(() => console.log("Connected to MongoDB!"))
  .catch((err) => console.error("Error connecting to MongoDB", err));

Important: Create a .env file in the root of your server directory and provide your MongoDB connection string:

MONGO_URI="mongodb://localhost:27017/mycruddb"
PORT=4000

(If using MongoDB Atlas, replace the local URI with your cluster’s URI.)

Creating the Data Model

Let’s assume we want to manage a simple User resource. Create a file named User.js in a new models folder.

mkdir models
touch models/User.js

models/User.js:

const mongoose = require("mongoose");
 
const userSchema = new mongoose.Schema(
  {
    name: {
      type: String,
      required: true,
    },
    email: {
      type: String,
      unique: true,
      required: true,
    },
    age: {
      type: Number,
      default: 0,
    },
  },
  { timestamps: true }
);
 
module.exports = mongoose.model("User", userSchema);

Building the CRUD API Endpoints

Create a file named routes.js to handle your CRUD routes.

touch routes.js

routes.js:

const express = require("express");
const router = express.Router();
const User = require("./models/User");
 
// CREATE
router.post("/users", async (req, res) => {
  try {
    const newUser = await User.create(req.body);
    res.status(201).json(newUser);
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});
 
// READ ALL
router.get("/users", async (req, res) => {
  try {
    const users = await User.find({});
    res.status(200).json(users);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});
 
// READ ONE
router.get("/users/:id", async (req, res) => {
  try {
    const user = await User.findById(req.params.id);
    if (!user) return res.status(404).json({ error: "User not found" });
    res.status(200).json(user);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});
 
// UPDATE
router.put("/users/:id", async (req, res) => {
  try {
    const updatedUser = await User.findByIdAndUpdate(req.params.id, req.body, {
      new: true,
    });
    if (!updatedUser) return res.status(404).json({ error: "User not found" });
    res.status(200).json(updatedUser);
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});
 
// DELETE
router.delete("/users/:id", async (req, res) => {
  try {
    const deletedUser = await User.findByIdAndDelete(req.params.id);
    if (!deletedUser) return res.status(404).json({ error: "User not found" });
    res.status(200).json(deletedUser);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});
 
module.exports = router;

Finally, import and use these routes in your index.js:

const routes = require("./routes");
 
// ...
 
app.use("/api", routes);

This means all your routes will be prefixed with /api. For example, GET /api/users.


4. Frontend Configuration

Scaffolding the React + Vite Project

  1. Create a new folder for your frontend:

    cd ..
    mkdir client
    cd client
  2. Initialize a Vite + React project:

    npm create vite@latest
    # or
    pnpm dlx create-vite
    # or
    yarn create vite
  3. Follow the prompts and choose React. Then, install dependencies:

    npm install
    # or
    pnpm install
    # or
    yarn
  4. Project structure after creation:

    client/
    ├─ index.html
    ├─ package.json
    ├─ vite.config.js
    └─ src/
        ├─ App.jsx
        ├─ main.jsx
        └─ ...
    

Project Structure

Here’s a suggested folder structure for the front end:

client/
  ├─ public/
  ├─ src/
  │   ├─ components/
  │   ├─ pages/
  │   ├─ services/
  │   ├─ App.jsx
  │   └─ main.jsx
  ├─ package.json
  └─ vite.config.js

Installing Additional Dependencies

You may want to install the following:

npm install axios react-router-dom
# or
pnpm add axios react-router-dom
# or
yarn add axios react-router-dom
  • axios: A promise-based HTTP client for making requests to our Node.js API.
  • react-router-dom: Enables client-side routing in React applications.

5. Implementing CRUD in React

Fetching Data from the API

Open App.jsx (or create a dedicated Users.jsx component in pages/Users.jsx) to fetch users from the API:

import { useEffect, useState } from "react";
import axios from "axios";
 
function Users() {
  const [users, setUsers] = useState([]);
 
  useEffect(() => {
    const getUsers = async () => {
      try {
        const response = await axios.get("http://localhost:4000/api/users");
        setUsers(response.data);
      } catch (error) {
        console.error("Error fetching users:", error);
      }
    };
    getUsers();
  }, []);
 
  return (
    <div>
      <h2>All Users</h2>
      <ul>
        {users.map((u) => (
          <li key={u._id}>
            {u.name} | {u.email} | {u.age}
          </li>
        ))}
      </ul>
    </div>
  );
}
 
export default Users;

Creating New Records

Add a form to create a new user. For simplicity, we’ll place it in the same component:

function Users() {
  // ...
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");
  const [age, setAge] = useState("");
 
  const handleCreate = async (e) => {
    e.preventDefault();
    try {
      const newUser = { name, email, age };
      await axios.post("http://localhost:4000/api/users", newUser);
      // Refetch users or update state
      const response = await axios.get("http://localhost:4000/api/users");
      setUsers(response.data);
      // Clear form
      setName("");
      setEmail("");
      setAge("");
    } catch (error) {
      console.error("Error creating user:", error);
    }
  };
 
  return (
    <div>
      {/* Form to create new user */}
      <form onSubmit={handleCreate}>
        <input
          placeholder="Name"
          value={name}
          onChange={(e) => setName(e.target.value)}
        />
        <input
          placeholder="Email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
        />
        <input
          placeholder="Age"
          type="number"
          value={age}
          onChange={(e) => setAge(e.target.value)}
        />
        <button type="submit">Create User</button>
      </form>
 
      <h2>All Users</h2>
      <ul>
        {users.map((u) => (
          <li key={u._id}>
            {u.name} | {u.email} | {u.age}
          </li>
        ))}
      </ul>
    </div>
  );
}

Updating Existing Records

To update a user, you can add an edit button and form. One approach is to maintain an “edit mode” and a separate state for the user being edited:

function Users() {
  // ...
  const [editingUserId, setEditingUserId] = useState(null);
  const [editingName, setEditingName] = useState("");
  const [editingEmail, setEditingEmail] = useState("");
  const [editingAge, setEditingAge] = useState("");
 
  const handleEdit = (user) => {
    setEditingUserId(user._id);
    setEditingName(user.name);
    setEditingEmail(user.email);
    setEditingAge(user.age);
  };
 
  const handleUpdate = async (e) => {
    e.preventDefault();
    try {
      await axios.put(`http://localhost:4000/api/users/${editingUserId}`, {
        name: editingName,
        email: editingEmail,
        age: editingAge,
      });
      // Refetch or update state
      const response = await axios.get("http://localhost:4000/api/users");
      setUsers(response.data);
      setEditingUserId(null);
    } catch (error) {
      console.error("Error updating user:", error);
    }
  };
 
  // ...
  return (
    // ...
    <ul>
      {users.map((u) => (
        <li key={u._id}>
          {u._id === editingUserId ? (
            <form onSubmit={handleUpdate}>
              <input
                value={editingName}
                onChange={(e) => setEditingName(e.target.value)}
              />
              <input
                value={editingEmail}
                onChange={(e) => setEditingEmail(e.target.value)}
              />
              <input
                type="number"
                value={editingAge}
                onChange={(e) => setEditingAge(e.target.value)}
              />
              <button type="submit">Update</button>
            </form>
          ) : (
            <>
              {u.name} | {u.email} | {u.age}{" "}
              <button onClick={() => handleEdit(u)}>Edit</button>
            </>
          )}
        </li>
      ))}
    </ul>
  );
}

Deleting Records

To delete a user:

const handleDelete = async (id) => {
  try {
    await axios.delete(`http://localhost:4000/api/users/${id}`);
    // Refetch users
    const response = await axios.get("http://localhost:4000/api/users");
    setUsers(response.data);
  } catch (error) {
    console.error("Error deleting user:", error);
  }
};
 
// ...
return (
  <ul>
    {users.map((u) => (
      <li key={u._id}>
        {u.name} | {u.email} | {u.age}
        <button onClick={() => handleDelete(u._id)}>Delete</button>
      </li>
    ))}
  </ul>
);

6. Testing the Application

  1. Start the Server:

    cd server
    npm run start
    # or
    pnpm start
    # or
    yarn start
  2. Start the Frontend:

    cd ../client
    npm run dev
    # or
    pnpm dev
    # or
    yarn dev
  3. Open the App: Navigate to http://localhost:5173 (or the port provided by Vite).

  4. Test CRUD:

    • Create users via the form.
    • View them in the list.
    • Edit an existing user.
    • Delete a user.

7. Deployment and Best Practices

Deployment Steps

  1. Production Build (Front-End):

    cd client
    npm run build
    # or
    pnpm build
    # or
    yarn build

    This generates a production-ready bundle in the dist folder.

  2. Configure the Backend to Serve Static Files (optional if you host separately).

    // In index.js
    const path = require("path");
    app.use(express.static(path.join(__dirname, "..", "client", "dist")));
  3. Environment Variables: Use services like Heroku, Railway, or Render for hosting. Make sure to set your MONGO_URI and PORT in their environment variable configurations.

  4. MongoDB Hosting: Use MongoDB Atlas for a managed, scalable database.

Security and Best Practices

  • Use HTTPS in production.
  • Sanitize inputs to prevent malicious data from entering your database.
  • Use Access Control / Authentication (e.g., JWT) if you need protected routes.
  • Use Logging (e.g., Morgan, Winston) for debugging and monitoring.
  • Version Control: Keep your project in a Git repository (GitHub, GitLab, etc.).

8. Conclusion

Congratulations! You’ve built a complete CRUD application with React, Vite, Node.js, and MongoDB using up-to-date best practices for 2024. By applying this stack, you gain a solid foundation for building and scaling modern web applications. Here’s a quick recap:

  • Backend: Node.js + Express + Mongoose for efficient, scalable APIs.
  • Frontend: React + Vite for a speedy development and build process.
  • CRUD: Create, Read, Update, and Delete user data end-to-end.
  • Deployment: Steps to package and deploy your app in a production environment.

Feel free to expand this project with authentication, validation, or advanced features like pagination and search. If you found this tutorial helpful, be sure to subscribe to my YouTube channel Pedrotechnologies for more in-depth tutorials and demos.

Happy coding!