
Complete MERN Stack Tutorial 2024: Build a Full-Stack Application from Scratch
// Middleware app.use(cors()); app.use(express.json());
// MongoDB Connection mongoose.connect(process.env.MONGODB_URI, { useNewUrlParser: true, useUnifiedTopology: true, }) .then(() => console.log('ā MongoDB Connected')) .catch((err) => console.error('ā MongoDB Error:', err));
// Routes app.use('/api/tasks', require('./routes/tasks'));
// Basic route app.get('/', (req, res) => { res.json({ message: 'Welcome to MERN Task API' }); });
// Start server
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => {
console.log(š Server running on port ${PORT});
});
</pre><h3>Step 5: Create Task Model (models/Task.js)</h3><pre class="ql-syntax" spellcheck="false">const mongoose = require('mongoose');
const TaskSchema = new mongoose.Schema({ title: { type: String, required: [true, 'Please add a title'], trim: true, maxlength: [100, 'Title cannot be more than 100 characters'] }, description: { type: String, required: [true, 'Please add a description'], maxlength: [500, 'Description cannot be more than 500 characters'] }, status: { type: String, enum: ['pending', 'in-progress', 'completed'], default: 'pending' }, priority: { type: String, enum: ['low', 'medium', 'high'], default: 'medium' }, dueDate: { type: Date, required: false }, createdAt: { type: Date, default: Date.now } });
module.exports = mongoose.model('Task', TaskSchema); </pre><h3>Step 6: Create API Routes (routes/tasks.js)</h3><pre class="ql-syntax" spellcheck="false">const express = require('express'); const router = express.Router(); const Task = require('../models/Task');
// @route GET /api/tasks // @desc Get all tasks router.get('/', async (req, res) => { try { const tasks = await Task.find().sort({ createdAt: -1 }); res.json({ success: true, count: tasks.length, data: tasks }); } catch (error) { res.status(500).json({ success: false, error: 'Server Error' }); } });
// @route GET /api/tasks/:id // @desc Get single task router.get('/:id', async (req, res) => { try { const task = await Task.findById(req.params.id); if (!task) { return res.status(404).json({ success: false, error: 'Task not found' }); } res.json({ success: true, data: task }); } catch (error) { res.status(500).json({ success: false, error: 'Server Error' }); } });
// @route POST /api/tasks // @desc Create new task router.post('/', async (req, res) => { try { const task = await Task.create(req.body); res.status(201).json({ success: true, data: task }); } catch (error) { if (error.name === 'ValidationError') { const messages = Object.values(error.errors).map(err => err.message); return res.status(400).json({ success: false, error: messages }); } res.status(500).json({ success: false, error: 'Server Error' }); } });
// @route PUT /api/tasks/:id // @desc Update task router.put('/:id', async (req, res) => { try { const task = await Task.findByIdAndUpdate( req.params.id, req.body, { new: true, runValidators: true } ); if (!task) { return res.status(404).json({ success: false, error: 'Task not found' }); } res.json({ success: true, data: task }); } catch (error) { res.status(500).json({ success: false, error: 'Server Error' }); } });
// @route DELETE /api/tasks/:id // @desc Delete task router.delete('/:id', async (req, res) => { try { const task = await Task.findByIdAndDelete(req.params.id); if (!task) { return res.status(404).json({ success: false, error: 'Task not found' }); } res.json({ success: true, data: {} }); } catch (error) { res.status(500).json({ success: false, error: 'Server Error' }); } });
module.exports = router; </pre><h3>Step 7: Environment Variables (.env)</h3><pre class="ql-syntax" spellcheck="false">MONGODB_URI=mongodb://localhost:27017/mern-tasks PORT=5000 </pre><h3>Step 8: Update package.json Scripts</h3><pre class="ql-syntax" spellcheck="false">{ "scripts": { "start": "node server.js", "dev": "nodemon server.js" } } </pre><h3>Step 9: Test the Backend</h3><pre class="ql-syntax" spellcheck="false"># Start the server npm run dev
Test with curl or Postman
curl http://localhost:5000/api/tasks </pre><h2>4. Creating the Frontend</h2><h3>Step 1: Create React App</h3><pre class="ql-syntax" spellcheck="false">cd ../frontend npx create-react-app . </pre><h3>Step 2: Install Dependencies</h3><pre class="ql-syntax" spellcheck="false">npm install axios react-router-dom </pre><h3>Step 3: Create Folder Structure</h3><pre class="ql-syntax" spellcheck="false">frontend/src/ āāā components/ ā āāā TaskList.js ā āāā TaskForm.js ā āāā TaskItem.js āāā services/ ā āāā api.js āāā App.js āāā index.js </pre><h3>Step 4: Create API Service (services/api.js)</h3><pre class="ql-syntax" spellcheck="false">import axios from 'axios';
const API_URL = 'http://localhost:5000/api';
const api = axios.create({ baseURL: API_URL, headers: { 'Content-Type': 'application/json' } });
export const taskAPI = {
// Get all tasks
getAllTasks: async () => {
const response = await api.get('/tasks');
return response.data;
},
// Get single task
getTask: async (id) => {
const response = await api.get(/tasks/${id});
return response.data;
},
// Create task
createTask: async (taskData) => {
const response = await api.post('/tasks', taskData);
return response.data;
},
// Update task
updateTask: async (id, taskData) => {
const response = await api.put(/tasks/${id}, taskData);
return response.data;
},
// Delete task
deleteTask: async (id) => {
const response = await api.delete(/tasks/${id});
return response.data;
}
};
export default api; </pre><h3>Step 5: Create Task Form Component (components/TaskForm.js)</h3><pre class="ql-syntax" spellcheck="false">import { useState } from 'react'; function TaskForm({ onSubmit, initialData = null }) { const [formData, setFormData] = useState({ title: initialData?.title || '', description: initialData?.description || '', status: initialData?.status || 'pending', priority: initialData?.priority || 'medium', dueDate: initialData?.dueDate ? new Date(initialData.dueDate).toISOString().split('T')[0] : '' });
const handleChange = (e) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
};
const handleSubmit = (e) => {
e.preventDefault();
onSubmit(formData);
if (!initialData) {
setFormData({ title: '', description: '', status: 'pending', priority: 'medium', dueDate: '' });
}
};
return (
<form onSubmit={handleSubmit} className="task-form">
<div className="form-group">
<label>Title</label>
<input
type="text"
name="title"
value={formData.title}
onChange={handleChange}
required
/>
</div>
<div className="form-group">
<label>Description</label>
<textarea
name="description"
value={formData.description}
onChange={handleChange}
rows="3"
required
></textarea>
</div>
<div className="form-row">
<div className="form-group">
<label>Status</label>
<select
name="status"
value={formData.status}
onChange={handleChange}
>
<option value="pending">Pending</option>
<option value="in-progress">In Progress</option>
<option value="completed">Completed</option>
</select>
</div>
<div className="form-group">
<label>Priority</label>
<select
name="priority"
value={formData.priority}
onChange={handleChange}
>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
</div>
</div>
<div className="form-group">
<label>Due Date</label>
<input
type="date"
name="dueDate"
value={formData.dueDate}
onChange={handleChange}
/>
</div>
<button type="submit" className="btn btn-primary">
{initialData ? 'Update Task' : 'Create Task'}
</button>
</form>
);
} export default TaskForm; </pre><h3>Step 6: Create Task Item Component (components/TaskItem.js)</h3><pre class="ql-syntax" spellcheck="false">// components/TaskItem.js function TaskItem({ task, onDelete, onUpdate }) { const getPriorityColor = (priority) => { switch (priority) { case 'high': return '#ef4444'; case 'medium': return '#f59e0b'; case 'low': return '#10b981'; default: return '#6b7280'; } };
const getStatusBadge = (status) => {
const badges = {
'pending': 'ā³ Pending',
'in-progress': 'š In Progress',
'completed': 'ā
Completed'
};
return badges[status] || status;
};
return (
<div className="task-item">
<div className="task-header">
<h3>{task.title}</h3>
<span
className="priority-badge"
style={{ backgroundColor: getPriorityColor(task.priority) }}
>
{task.priority}
</span>
</div>
<p className="task-description">{task.description}</p>
<div className="task-meta">
<span className="status-badge">{getStatusBadge(task.status)}</span>
{task.dueDate && (
<span className="due-date">
š
{new Date(task.dueDate).toLocaleDateString()}</span>
)}
</div>
<div className="task-actions">
<button
onClick={() => onUpdate(task)}
className="btn btn-edit"
>
Edit
</button>
<button
onClick={() => onDelete(task._id)}
className="btn btn-delete"
>
Delete
</button>
</div>
</div>
);
}
export default TaskItem; </pre><h3>Step 7: Create Main App Component (App.js)</h3><pre class="ql-syntax" spellcheck="false">// App.js import { useState, useEffect } from 'react'; import { taskAPI } from './services/api'; import TaskForm from './components/TaskForm'; import TaskItem from './components/TaskItem'; import './App.css';
function App() { const [tasks, setTasks] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [editingTask, setEditingTask] = useState(null);
// Fetch tasks on component mount
useEffect(() => {
fetchTasks();
}, []);
const fetchTasks = async () => {
try {
setLoading(true);
const response = await taskAPI.getAllTasks();
setTasks(response.data);
setError(null);
} catch (err) {
setError('Failed to fetch tasks');
console.error(err);
} finally {
setLoading(false);
}
};
const handleCreateTask = async (taskData) => {
try {
await taskAPI.createTask(taskData);
fetchTasks();
} catch (err) {
setError('Failed to create task');
console.error(err);
}
};
const handleUpdateTask = async (taskData) => {
try {
// Need to handle the dueDate formatting before sending to API
const formattedTaskData = {
...taskData,
dueDate: taskData.dueDate ? new Date(taskData.dueDate).toISOString() : undefined
};
await taskAPI.updateTask(editingTask._id, formattedTaskData);
setEditingTask(null);
fetchTasks();
} catch (err) {
setError('Failed to update task');
console.error(err);
}
};
const handleDeleteTask = async (id) => {
if (window.confirm('Are you sure you want to delete this task?')) {
try {
await taskAPI.deleteTask(id);
fetchTasks();
} catch (err) {
setError('Failed to delete task');
console.error(err);
}
}
};
if (loading) {
return <div className="loading">Loading tasks...</div>;
}
return (
<div className="App">
<header className="app-header">
<h1>š MERN Task Manager</h1>
<p>Manage your tasks efficiently</p>
</header>
{error && (
<div className="error-message">
{error}
<button onClick={() => setError(null)}>ā</button>
</div>
)}
<div className="container">
<div className="form-section">
<h2>{editingTask ? 'Edit Task' : 'Create New Task'}</h2>
<TaskForm
onSubmit={editingTask ? handleUpdateTask : handleCreateTask}
initialData={editingTask}
/>
{editingTask && (
<button
onClick={() => setEditingTask(null)}
className="btn btn-cancel"
>
Cancel Edit
</button>
)}
</div>
<div className="tasks-section">
<h2>Your Tasks ({tasks.length})</h2>
{tasks.length === 0 ? (
<p className="no-tasks">No tasks yet. Create one to get started!</p>
) : (
<div className="tasks-grid">
{tasks.map(task => (
<TaskItem
key={task._id}
task={task}
onDelete={handleDeleteTask}
onUpdate={setEditingTask}
/>
))}
</div>
)}
</div>
</div>
</div>
);
}
export default App; </pre><h3>Step 8: Add Styling (App.css)</h3><pre class="ql-syntax" spellcheck="false">* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; }
.App { max-width: 1400px; margin: 0 auto; padding: 20px; }
.app-header { text-align: center; color: white; margin-bottom: 40px; }
.app-header h1 { font-size: 3rem; margin-bottom: 10px; }
.container { display: grid; grid-template-columns: 400px 1fr; gap: 30px; }
.form-section, .tasks-section { background: white; padding: 30px; border-radius: 15px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); }
.form-section h2, .tasks-section h2 { margin-bottom: 20px; color: #333; }
.task-form { display: flex; flex-direction: column; gap: 15px; }
.form-group { display: flex; flex-direction: column; }
.form-group label { margin-bottom: 5px; font-weight: 600; color: #555; }
.form-group input, .form-group textarea, .form-group select { padding: 10px; border: 2px solid #e0e0e0; border-radius: 8px; font-size: 14px; transition: border-color 0.3s; }
.form-group input:focus, .form-group textarea:focus, .form-group select:focus { outline: none; border-color: #667eea; }
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; }
.btn { padding: 12px 24px; border: none; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer; transition: all 0.3s; }
.btn-primary { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; }
.btn-primary:hover { transform: translateY(-2px); box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4); }
.tasks-grid { display: grid; gap: 20px; }
.task-item { background: #f9fafb; padding: 20px; border-radius: 10px; border-left: 4px solid #667eea; transition: transform 0.3s; }
.task-item:hover { transform: translateX(5px); }
.task-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
.task-header h3 { color: #333; font-size: 1.2rem; }
.priority-badge { padding: 4px 12px; border-radius: 20px; color: white; font-size: 12px; font-weight: 600; text-transform: uppercase; }
.task-description { color: #666; margin-bottom: 15px; line-height: 1.6; }
.task-meta { display: flex; gap: 15px; margin-bottom: 15px; }
.status-badge { padding: 4px 12px; background: #e0e7ff; color: #4f46e5; border-radius: 20px; font-size: 12px; font-weight: 600; }
.due-date { color: #666; font-size: 14px; }
.task-actions { display: flex; gap: 10px; }
.btn-edit { background: #10b981; color: white; }
.btn-delete { background: #ef4444; color: white; }
.btn-cancel { background: #6b7280; color: white; margin-top: 10px; }
.loading { text-align: center; color: white; font-size: 1.5rem; padding: 50px; }
.error-message { background: #fee2e2; color: #991b1b; padding: 15px; border-radius: 8px; margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center; }
.no-tasks { text-align: center; color: #999; padding: 40px; font-size: 1.1rem; }
@media (max-width: 1024px) { .container { grid-template-columns: 1fr; } } </pre><h2>5. Running the Complete Application</h2><h3>Terminal 1 - Backend</h3><pre class="ql-syntax" spellcheck="false">cd backend npm run dev </pre><h3>Terminal 2 - Frontend</h3><pre class="ql-syntax" spellcheck="false">cd frontend npm start </pre><p>Visit http://localhost:3000 to see your app!</p><h2>6. Deployment Guide</h2><h3>Deploy Backend (Heroku)</h3><pre class="ql-syntax" spellcheck="false"># Install Heroku CLI
Create Procfile
echo "web: node server.js" > Procfile
Deploy
heroku create your-app-name git push heroku main </pre><h3>Deploy Frontend (Vercel)</h3><pre class="ql-syntax" spellcheck="false"># Install Vercel CLI npm i -g vercel
Deploy
cd frontend vercel </pre><h3>Use MongoDB Atlas</h3><ol><li>Create account at mongodb.com/cloud/atlas</li><li>Create cluster</li><li>Get connection string</li><li>Update .env with Atlas URI</li></ol><h2>7. Best Practices</h2><h3>Security</h3><ul><li>Use environment variables</li><li>Implement authentication (JWT)</li><li>Validate all inputs</li><li>Use HTTPS in production</li><li>Implement rate limiting</li></ul><h3>Performance</h3><ul><li>Use indexes in MongoDB</li><li>Implement pagination</li><li>Cache frequently accessed data</li><li>Optimize images</li><li>Use CDN for static assets</li></ul><h3>Code Quality</h3><ul><li>Use ESLint and Prettier</li><li>Write unit tests</li><li>Use TypeScript</li><li>Follow naming conventions</li><li>Document your code</li></ul><h2>Conclusion</h2><p>Congratulations! You've built a complete MERN stack application. You now know how to:</p><ul><li>ā Set up Node.js backend with Express</li><li>ā Create MongoDB models and schemas</li><li>ā Build RESTful APIs</li><li>ā Create React frontend</li><li>ā Connect frontend and backend</li><li>ā Deploy full-stack applications</li></ul><h3>Next Steps</h3><ul><li>Add user authentication</li><li>Implement real-time updates with Socket.io</li><li>Add file upload functionality</li><li>Create mobile app with React Native</li><li>Learn advanced MongoDB queries</li></ul>