MERN Stack Architecture
Building Full-Stack Apps with MongoDB, Express, React, Node
Explanation
What is the MERN Stack?
MERN is a full-stack JavaScript framework combining four technologies:
- MongoDB: NoSQL database for storing data as JSON-like documents
- Express: Backend web framework for Node.js
- React: Frontend library for building user interfaces
- Node.js: JavaScript runtime for the server
Think of it like building a house:
- MongoDB is the foundation (data storage)
- Express is the plumbing and electrical (backend logic)
- React is the interior design (what users see)
- Node.js is the construction crew (runs everything)
Why MERN?
- One Language: JavaScript everywhere (frontend + backend)
- JSON Everywhere: Data flows as JSON from DB to UI
- Large Ecosystem: npm has packages for everything
- Hot Job Market: MERN skills are highly sought after
Architecture Overview
┌─────────────────────────────────────────────┐
│ FRONTEND │
│ React (Port 3000) │
│ Components, State, API Calls │
└────────────────────┬────────────────────────┘
│ HTTP Requests (fetch/axios)
▼
┌─────────────────────────────────────────────┐
│ BACKEND │
│ Express + Node.js (Port 5000) │
│ Routes, Controllers, Middleware │
└────────────────────┬────────────────────────┘
│ Mongoose ODM
▼
┌─────────────────────────────────────────────┐
│ DATABASE │
│ MongoDB (Port 27017) │
│ Collections, Documents, Indexes │
└─────────────────────────────────────────────┘
Demonstration
Example 1: Backend Setup (Express + MongoDB)
// server.js
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
require('dotenv').config();
const app = express();
// Middleware
app.use(cors());
app.use(express.json());
// MongoDB Connection
mongoose.connect(process.env.MONGODB_URI)
.then(() => console.log('MongoDB Connected'))
.catch(err => console.error('MongoDB Error:', err));
// User Model
const userSchema = new mongoose.Schema({
name: { type: String, required: true },
email: { type: String, required: true, unique: true },
createdAt: { type: Date, default: Date.now }
});
const User = mongoose.model('User', userSchema);
// Routes
app.get('/api/users', async (req, res) => {
try {
const users = await User.find();
res.json(users);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.post('/api/users', async (req, res) => {
try {
const user = new User(req.body);
await user.save();
res.status(201).json(user);
} catch (error) {
res.status(400).json({ error: error.message });
}
});
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
Example 2: Frontend (React)
// App.jsx
import { useState, useEffect } from 'react';
function App() {
const [users, setUsers] = useState([]);
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [loading, setLoading] = useState(true);
// Fetch users on mount
useEffect(() => {
fetchUsers();
}, []);
const fetchUsers = async () => {
try {
const response = await fetch('http://localhost:5000/api/users');
const data = await response.json();
setUsers(data);
} catch (error) {
console.error('Error fetching users:', error);
} finally {
setLoading(false);
}
};
const handleSubmit = async (e) => {
e.preventDefault();
try {
const response = await fetch('http://localhost:5000/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, email })
});
if (response.ok) {
setName('');
setEmail('');
fetchUsers(); // Refresh the list
}
} catch (error) {
console.error('Error creating user:', error);
}
};
if (loading) return <div>Loading...</div>;
return (
<div className="app">
<h1>User Management</h1>
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="Name"
value={name}
onChange={(e) => setName(e.target.value)}
required
/>
<input
type="email"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
<button type="submit">Add User</button>
</form>
<ul>
{users.map(user => (
<li key={user._id}>
{user.name} - {user.email}
</li>
))}
</ul>
</div>
);
}
export default App;
Key Takeaways:
- Backend runs on one port (5000), frontend on another (3000)
- CORS middleware allows cross-origin requests
- React fetches data using
fetch()or axios - MongoDB documents have
_id(notid)
Imitation
Challenge 1: Add Delete Functionality
Task: Add a delete button for each user that removes them from the database.
Solution
Backend route:
app.delete('/api/users/:id', async (req, res) => {
try {
await User.findByIdAndDelete(req.params.id);
res.status(204).send();
} catch (error) {
res.status(500).json({ error: error.message });
}
});
Frontend:
const handleDelete = async (id) => {
await fetch(`http://localhost:5000/api/users/${id}`, {
method: 'DELETE'
});
fetchUsers();
};
// In the JSX:
<button onClick={() => handleDelete(user._id)}>Delete</button>
Challenge 2: Add Error Handling
Task: Display error messages to the user when API calls fail.
Practice
Exercise 1: Build a Todo App
Difficulty: Beginner
Build a complete MERN todo app with:
- Add todos
- Mark as complete
- Delete todos
- Filter by status
Exercise 2: Add Authentication
Difficulty: Advanced
Add user authentication with:
- Registration endpoint
- Login with JWT
- Protected routes
- Logout functionality
Summary
What you learned:
- MERN stack components and how they connect
- Setting up Express with MongoDB
- Building React frontend to consume APIs
- Full CRUD operations across the stack
Next Steps:
- Read: SSR vs CSR
- Practice: Build a blog with MERN
- Deploy: Put your MERN app on Railway or Render
