Full-Stack6 min read
Introduction to the MERN Stack
What MongoDB, Express.js, React, and Node.js each do, how they work together, and a minimal example of a REST API with Mongoose connected to a React frontend.
By Nitin Wagmode
MERN is a popular set of technologies for building full-stack web applications entirely in JavaScript:
- MongoDB: a document database
- Express.js: a web framework for Node.js
- React: a library for building user interfaces
- Node.js: a JavaScript runtime for running code on the server
Because every layer uses JavaScript (or TypeScript), one developer or team can work across the frontend, backend, and data layer without switching languages.
How the pieces fit together
A typical request flows like this:
- The user interacts with the React app in the browser.
- React sends an HTTP request to an API, for example
GET /api/tasks. - An Express server running on Node.js receives the request and runs the matching route.
- The route reads or writes data in MongoDB.
- Express sends a JSON response, and React updates the UI.
The frontend and backend communicate only through the API, so each can be developed, tested, and deployed separately.
MongoDB
MongoDB stores data as documents, which look like JSON objects, grouped into collections:
{
"_id": "665f1c2e9b1e8a3d4c2b1a90",
"title": "Write project README",
"done": false,
"createdAt": "2026-10-20T09:30:00.000Z"
}Documents in the same collection do not need identical fields, which makes it easy to change data shapes as an app evolves. MongoDB can run locally or as a managed service such as MongoDB Atlas.
Mongoose is a popular library for working with MongoDB in Node.js. It adds schemas, validation, and a convenient query API.
Node.js and Express
Node.js runs JavaScript outside the browser. Express is a minimal framework on top of Node.js for defining routes and middleware.
A minimal backend
Set up a project and install dependencies:
mkdir mern-tasks-api && cd mern-tasks-api
npm init -y
npm install express mongoose cors dotenvDefine a model with Mongoose:
// models/Task.js
const mongoose = require("mongoose");
const taskSchema = new mongoose.Schema(
{
title: { type: String, required: true, trim: true, maxlength: 200 },
done: { type: Boolean, default: false },
},
{ timestamps: true }
);
module.exports = mongoose.model("Task", taskSchema);Create the server with a few REST routes:
// server.js
require("dotenv").config();
const express = require("express");
const mongoose = require("mongoose");
const cors = require("cors");
const Task = require("./models/Task");
const app = express();
app.use(express.json());
app.use(cors({ origin: process.env.CLIENT_ORIGIN }));
app.get("/api/tasks", async (req, res, next) => {
try {
const tasks = await Task.find().sort({ createdAt: -1 });
res.json(tasks);
} catch (error) {
next(error);
}
});
app.post("/api/tasks", async (req, res, next) => {
try {
const task = await Task.create({ title: req.body.title });
res.status(201).json(task);
} catch (error) {
if (error.name === "ValidationError") {
return res.status(400).json({ error: "Title is required." });
}
next(error);
}
});
app.patch("/api/tasks/:id", async (req, res, next) => {
try {
const task = await Task.findByIdAndUpdate(
req.params.id,
{ done: Boolean(req.body.done) },
{ new: true, runValidators: true }
);
if (!task) return res.status(404).json({ error: "Task not found." });
res.json(task);
} catch (error) {
next(error);
}
});
// Central error handler: log details on the server, send a generic message
app.use((error, req, res, next) => {
console.error(error);
res.status(500).json({ error: "Something went wrong." });
});
mongoose
.connect(process.env.MONGODB_URI)
.then(() => app.listen(process.env.PORT || 4000, () => console.log("API running")))
.catch((error) => {
console.error("Database connection failed", error);
process.exit(1);
});Keep the connection string in an .env file that is excluded from version control:
# .env (never commit this file)
MONGODB_URI=mongodb://127.0.0.1:27017/mern-tasks
CLIENT_ORIGIN=http://localhost:5173
PORT=4000A few practices in this example are worth keeping:
express.json()parses JSON request bodies.corsallows only your frontend's origin to call the API from a browser.- Validation happens on the server; the frontend's checks are for user experience only.
- Errors return generic messages, so internal details are not exposed to users.
The React frontend
On the frontend, React fetches data from the API and renders it:
import { useEffect, useState } from "react";
const API_URL = import.meta.env.VITE_API_URL; // e.g. http://localhost:4000
export default function Tasks() {
const [tasks, setTasks] = useState([]);
const [title, setTitle] = useState("");
const [error, setError] = useState("");
useEffect(() => {
fetch(`${API_URL}/api/tasks`)
.then((response) => response.json())
.then(setTasks)
.catch(() => setError("Could not load tasks."));
}, []);
async function addTask(event) {
event.preventDefault();
const response = await fetch(`${API_URL}/api/tasks`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title }),
});
if (!response.ok) return setError("Could not add task.");
const task = await response.json();
setTasks((previous) => [task, ...previous]);
setTitle("");
}
return (
<main>
<form onSubmit={addTask}>
<label htmlFor="title">New task</label>
<input id="title" value={title} onChange={(e) => setTitle(e.target.value)} />
<button type="submit">Add</button>
</form>
{error && <p role="alert">{error}</p>}
<ul>
{tasks.map((task) => (
<li key={task._id}>{task.title}</li>
))}
</ul>
</main>
);
}Project structure
As the app grows, separate concerns into folders:
server/
models/ Mongoose schemas
routes/ Express routers
middleware/ Auth, validation, error handling
server.js
client/
src/
components/
pages/
api/ Functions that call the backendWhat to add next
The example above is intentionally minimal. Real applications usually also need:
- Authentication: for example, sessions or JSON Web Tokens, with passwords hashed using a library like bcrypt.
- Input validation: a schema library such as Zod or Joi on the server.
- Security middleware: rate limiting and secure HTTP headers (for example, with Helmet).
- Testing: API tests with tools like Jest and Supertest, and component tests with React Testing Library.
- Deployment: hosting for the API and frontend, and a managed database.
Strengths and trade-offs
MERN is a good fit for many web apps, especially when you want one language across the stack and flexible data models. Like any stack, it involves trade-offs. Highly relational data with complex joins and strict transactional requirements may be modeled more naturally in a relational database such as PostgreSQL or MySQL, although MongoDB does support multi-document transactions. Choose based on your data and requirements rather than the stack's popularity.
Key takeaways
- MERN combines MongoDB, Express.js, React, and Node.js, all using JavaScript.
- React talks to an Express API over HTTP; Express reads and writes MongoDB.
- Mongoose adds schemas and validation on top of MongoDB.
- Validate on the server, keep secrets in environment variables, and avoid leaking error details.
Conclusion
The MERN stack is a practical way to learn how a full web application fits together, from the database to the UI. Building a small project, such as a task manager or notes app, is the best way to understand each layer and how they communicate.
- #MERN Stack
- #MongoDB
- #Express.js
- #React
- #Node.js
Keep reading
- Oct 24, 2026
MongoDB vs Firebase: Choosing the Right Data Solution
A balanced comparison of MongoDB and Firebase (Cloud Firestore): data models, querying, real-time updates, security, offline support, pricing models, and which projects suit each.6 min read - Oct 24, 2026
How to Test REST APIs Using Postman
A practical guide to testing REST APIs with Postman: requests, collections, environments and variables, authentication, test scripts, chaining requests, and running tests from the command line.5 min read - Oct 24, 2026
Design Systems for Consistent Web Interfaces
What a design system is, how design tokens, components, and guidelines fit together, and a practical way to start one in a React and CSS codebase.5 min read