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

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
- Introduction
- Setting Up the Environment
- Backend Configuration
- Frontend Configuration
- Implementing CRUD in React
- Testing the Application
- Deployment and Best Practices
- 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?
- Scalability: Node.js excels at handling concurrent requests, making it a popular choice for modern web apps.
- Flexibility: React + Vite provides a fast, modular front-end environment with minimal configuration overhead.
- Performance: MongoDB’s document-based storage is both flexible and performant.
- 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
- Node.js and npm: Download and install from nodejs.org.
- pnpm (recommended): Installation guide.
- MongoDB:
- Local: Install from mongodb.com.
- Cloud (MongoDB Atlas): Sign up and create a cluster.
3. Backend Configuration
Initializing the Node.js + Express Project
-
Create a new folder for your backend:
mkdir server cd server
-
Initialize a package.json file:
npm init -y # or pnpm init # or yarn init -y
-
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.
-
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 yourserver
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
-
Create a new folder for your frontend:
cd .. mkdir client cd client
-
Initialize a Vite + React project:
npm create vite@latest # or pnpm dlx create-vite # or yarn create vite
-
Follow the prompts and choose React. Then, install dependencies:
npm install # or pnpm install # or yarn
-
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
-
Start the Server:
cd server npm run start # or pnpm start # or yarn start
-
Start the Frontend:
cd ../client npm run dev # or pnpm dev # or yarn dev
-
Open the App: Navigate to http://localhost:5173 (or the port provided by Vite).
-
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
-
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. -
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")));
-
Environment Variables: Use services like Heroku, Railway, or Render for hosting. Make sure to set your
MONGO_URI
andPORT
in their environment variable configurations. -
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!