6 min read

Building a MERN Stack Application with MongoDB, Express.js, React, and Node.js

Table of Contents

The MERN stack—MongoDB, Express.js, React, and Node.js—is a powerful, full-stack JavaScript framework for building modern web applications. By leveraging JavaScript across the entire stack, developers can create scalable, efficient, and dynamic apps with a unified language. In this comprehensive guide, we’ll walk through building a simple MERN stack application (a task manager) with a minimum 5-minute read, covering setup, backend, frontend, and best practices. Let’s dive in! 🚀

Why Choose the MERN Stack?

The MERN stack combines four robust technologies:

  • MongoDB: A NoSQL database for flexible, scalable data storage using JSON-like documents.
  • Express.js: A lightweight Node.js framework for building RESTful APIs.
  • React: A frontend library for creating dynamic, component-based user interfaces.
  • Node.js: A server-side runtime for executing JavaScript, enabling scalable backend development.

This stack excels for its end-to-end JavaScript usage, vibrant community, and ability to handle everything from small projects to large-scale applications like e-commerce platforms or social media apps.

Setting Up the Development Environment

To start, ensure you have Node.js and MongoDB installed. You can use MongoDB locally or opt for MongoDB Atlas for a cloud-based solution. Create a project directory and initialize it:

mkdir mern-task-manager
cd mern-task-manager
npm init -y

Install essential tools like nodemon for auto-restarting the server and concurrently to run backend and frontend simultaneously.

Building the Backend with Node.js, Express.js, and MongoDB

Setting Up Express.js

Create a server folder for the backend:

mkdir server
cd server
npm init -y
npm install express mongoose dotenv cors

Set up the Express server in server/index.js:

const express = require("express");
const mongoose = require("mongoose");
const cors = require("cors");
require("dotenv").config();

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

mongoose
  .connect(process.env.MONGO_URI, {
    useNewUrlParser: true,
    useUnifiedTopology: true,
  })
  .then(() => console.log("MongoDB connected"));

app.get("/", (req, res) => res.send("API running"));

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

Create a .env file with your MongoDB connection string:

MONGO_URI=mongodb://localhost:27017/task-manager
PORT=5000

Defining the Task Model

In server/models/Task.js, define a MongoDB schema for tasks:

const mongoose = require("mongoose");

const taskSchema = new mongoose.Schema({
  title: { type: String, required: true },
  description: { type: String },
  completed: { type: Boolean, default: false },
  createdAt: { type: Date, default: Date.now },
});

module.exports = mongoose.model("Task", taskSchema);

Creating API Routes

In server/routes/tasks.js, set up CRUD routes:

const express = require("express");
const router = express.Router();
const Task = require("../models/Task");

// Get all tasks
router.get("/", async (req, res) => {
  try {
    const tasks = await Task.find();
    res.json(tasks);
  } catch (err) {
    res.status(500).json({ message: err.message });
  }
});

// Create a task
router.post("/", async (req, res) => {
  const task = new Task({
    title: req.body.title,
    description: req.body.description,
  });
  try {
    const newTask = await task.save();
    res.status(201).json(newTask);
  } catch (err) {
    res.status(400).json({ message: err.message });
  }
});

// Update a task
router.put("/:id", async (req, res) => {
  try {
    const task = await Task.findById(req.params.id);
    if (task) {
      task.title = req.body.title || task.title;
      task.description = req.body.description || task.description;
      task.completed = req.body.completed ?? task.completed;
      const updatedTask = await task.save();
      res.json(updatedTask);
    } else {
      res.status(404).json({ message: "Task not found" });
    }
  } catch (err) {
    res.status(400).json({ message: err.message });
  }
});

// Delete a task
router.delete("/:id", async (req, res) => {
  try {
    const task = await Task.findById(req.params.id);
    if (task) {
      await task.remove();
      res.json({ message: "Task deleted" });
    } else {
      res.status(404).json({ message: "Task not found" });
    }
  } catch (err) {
    res.status(500).json({ message: err.message });
  }
});

module.exports = router;

Update server/index.js to use the routes:

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

Run the backend with nodemon index.js.

Building the Frontend with React

Setting Up React

In the root directory, create a React app:

npx create-react-app client
cd client
npm install axios

Creating Components

Create a TaskList component in client/src/components/TaskList.js:

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

const TaskList = () => {
  const [tasks, setTasks] = useState([]);
  const [title, setTitle] = useState("");
  const [description, setDescription] = useState("");

  useEffect(() => {
    axios
      .get("http://localhost:5000/api/tasks")
      .then((res) => setTasks(res.data))
      .catch((err) => console.error(err));
  }, []);

  const addTask = async () => {
    try {
      const res = await axios.post("http://localhost:5000/api/tasks", {
        title,
        description,
      });
      setTasks([...tasks, res.data]);
      setTitle("");
      setDescription("");
    } catch (err) {
      console.error(err);
    }
  };

  const updateTask = async (id, updatedTask) => {
    try {
      const res = await axios.put(
        `http://localhost:5000/api/tasks/${id}`,
        updatedTask,
      );
      setTasks(tasks.map((task) => (task._id === id ? res.data : task)));
    } catch (err) {
      console.error(err);
    }
  };

  const deleteTask = async (id) => {
    try {
      await axios.delete(`http://localhost:5000/api/tasks/${id}`);
      setTasks(tasks.filter((task) => task._id !== id));
    } catch (err) {
      console.error(err);
    }
  };

  return (
    <div>
      <h1>Task Manager</h1>
      <input
        type="text"
        value={title}
        onChange={(e) => setTitle(e.target.value)}
        placeholder="Task title"
      />
      <input
        type="text"
        value={description}
        onChange={(e) => setDescription(e.target.value)}
        placeholder="Task description"
      />
      <button onClick={addTask}>Add Task</button>
      <ul>
        {tasks.map((task) => (
          <li key={task._id}>
            <input
              type="checkbox"
              checked={task.completed}
              onChange={() =>
                updateTask(task._id, { ...task, completed: !task.completed })
              }
            />
            {task.title} - {task.description}
            <button onClick={() => deleteTask(task._id)}>Delete</button>
          </li>
        ))}
      </ul>
    </div>
  );
};

export default TaskList;

Update client/src/App.js:

import React from "react";
import TaskList from "./components/TaskList";
import "./App.css";

function App() {
  return (
    <div className="App">
      <TaskList />
    </div>
  );
}

export default App;

Add basic styling in client/src/App.css:

.App {
  text-align: center;
  max-width: 600px;
  margin: 0 auto;
  padding: 20px;
}
input,
button {
  margin: 10px;
  padding: 5px;
}
ul {
  list-style: none;
  padding: 0;
}
li {
  margin: 10px 0;
}

Running the Application

Update the root package.json to run both servers:

"scripts": {
  "start": "concurrently \"npm run server\" \"npm run client\"",
  "server": "cd server && nodemon index.js",
  "client": "cd client && npm start"
}

Install concurrently in the root directory:

npm install concurrently

Run npm start to launch both the backend and frontend. Visit http://localhost:3000 to see the task manager in action.

Best Practices for MERN Development

To build robust MERN applications:

  1. Structure Your Code: Keep backend and frontend code separate for maintainability.
  2. Secure Your API: Use environment variables and implement authentication (e.g., JWT).
  3. Optimize Performance: Use MongoDB indexes and minimize API calls in React.
  4. Test Thoroughly: Use tools like Jest for React and Mocha for Node.js.
  5. Enhance with AI: Integrate AI features via xAI’s API for intelligent functionality.

What’s Next?

The MERN stack empowers developers to build modern, scalable applications. Stay tuned for more on:

  1. Adding authentication to MERN apps
  2. Optimizing MongoDB performance
  3. Building real-time features with WebSockets
  4. Web development trends for 2026

This task manager is just the start—expand it with features like user authentication or task categories to explore the full power of MERN!