Back to blog
Complete MERN Stack Tutorial 2024: Build a Full-Stack Application from Scratch

Complete MERN Stack Tutorial 2024: Build a Full-Stack Application from Scratch

Piyush Chauhan
•
November 6, 2025
•
12 min read
šŸ“‚ Full Stack Development
#MERN Stack#MongoDB#Express.js#React#Node.js#Full Stack Development#JavaScript#Web Development#Tutorial
<p>MERN Stack Task Management App TutorialThe MERN stack (MongoDB, Express.js, React, Node.js) is one of the most popular technology stacks for building modern web applications. In this comprehensive tutorial, you'll learn how to build a full-stack task management application from scratch.</p><h2>## Table of Contents</h2><ol><li><a href="#what-is-the-mern-stack" rel="noopener noreferrer" target="_blank">What is the MERN Stack?</a></li><li><a href="#prerequisites-and-setup" rel="noopener noreferrer" target="_blank">Prerequisites and Setup</a></li><li><a href="#building-the-backend" rel="noopener noreferrer" target="_blank">Building the Backend (Node.js + Express + MongoDB)</a></li><li><a href="#creating-the-frontend" rel="noopener noreferrer" target="_blank">Creating the Frontend (React)</a></li><li><a href="#running-the-complete-application" rel="noopener noreferrer" target="_blank">Running the Complete Application</a></li><li><a href="#deployment-guide" rel="noopener noreferrer" target="_blank">Deployment Guide</a></li><li><a href="#best-practices-and-tips" rel="noopener noreferrer" target="_blank">Best Practices and Tips</a></li></ol><h2>1. What is the MERN Stack?</h2><p>The MERN stack consists of four powerful technologies:</p><ul><li><strong>MongoDB</strong>: NoSQL database for storing data</li><li><strong>Express.js</strong>: Web application framework for Node.js</li><li><strong>React</strong>: Frontend library for building user interfaces</li><li><strong>Node.js</strong>: JavaScript runtime for server-side code</li></ul><h3>Why Choose MERN Stack?</h3><ul><li>āœ… <strong>JavaScript Everywhere</strong>: Use one language for both frontend and backend</li><li>āœ… <strong>Fast Development</strong>: Reusable components and rapid prototyping</li><li>āœ… <strong>Scalable</strong>: Handle millions of users with proper architecture</li><li>āœ… <strong>Large Community</strong>: Extensive resources and packages available</li><li>āœ… <strong>High Demand</strong>: Top skill in job market</li></ul><h2>2. Prerequisites and Setup</h2><h3>What You Need to Know</h3><ul><li>JavaScript fundamentals (ES6+)</li><li>Basic understanding of APIs and HTTP</li><li>Command line basics</li><li>Git basics</li></ul><h3>Required Software</h3><pre class="ql-syntax" spellcheck="false"># Check if Node.js is installed node --version # Should be v18+ or v20+ # Check if npm is installed npm --version # Install MongoDB (or use MongoDB Atlas) # Download from: https://www.mongodb.com/try/download/community </pre><h3>Project Setup</h3><pre class="ql-syntax" spellcheck="false"># Create project directory mkdir mern-task-app cd mern-task-app # Create backend and frontend folders mkdir backend frontend </pre><h2>3. Building the Backend</h2><h3>Step 1: Initialize Node.js Project</h3><pre class="ql-syntax" spellcheck="false">cd backend npm init -y </pre><h3>Step 2: Install Dependencies</h3><pre class="ql-syntax" spellcheck="false">npm install express mongoose dotenv cors npm install --save-dev nodemon </pre><p><strong>Package Explanation</strong>:</p><ul><li>express: Web framework</li><li>mongoose: MongoDB object modeling</li><li>dotenv: Environment variables</li><li>cors: Enable cross-origin requests</li><li>nodemon: Auto-restart server on changes</li></ul><h3>Step 3: Create Server Structure</h3><pre class="ql-syntax" spellcheck="false">backend/ ā”œā”€ā”€ models/ │ └── Task.js ā”œā”€ā”€ routes/ │ └── tasks.js ā”œā”€ā”€ .env ā”œā”€ā”€ server.js └── package.json </pre><h3>Step 4: Create Server (server.js)</h3><pre class="ql-syntax" spellcheck="false">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, { 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) =&gt; {
    setFormData({ ...formData, [e.target.name]: e.target.value });
};

const handleSubmit = (e) =&gt; {
    e.preventDefault();
    onSubmit(formData);
    if (!initialData) {
        setFormData({ title: '', description: '', status: 'pending', priority: 'medium', dueDate: '' });
    }
};

return (
    &lt;form onSubmit={handleSubmit} className="task-form"&gt;
        &lt;div className="form-group"&gt;
            &lt;label&gt;Title&lt;/label&gt;
            &lt;input
                type="text"
                name="title"
                value={formData.title}
                onChange={handleChange}
                required
            /&gt;
        &lt;/div&gt;
        &lt;div className="form-group"&gt;
            &lt;label&gt;Description&lt;/label&gt;
            &lt;textarea
                name="description"
                value={formData.description}
                onChange={handleChange}
                rows="3"
                required
            &gt;&lt;/textarea&gt;
        &lt;/div&gt;

        &lt;div className="form-row"&gt;
            &lt;div className="form-group"&gt;
                &lt;label&gt;Status&lt;/label&gt;
                &lt;select
                    name="status"
                    value={formData.status}
                    onChange={handleChange}
                &gt;
                    &lt;option value="pending"&gt;Pending&lt;/option&gt;
                    &lt;option value="in-progress"&gt;In Progress&lt;/option&gt;
                    &lt;option value="completed"&gt;Completed&lt;/option&gt;
                &lt;/select&gt;
            &lt;/div&gt;

            &lt;div className="form-group"&gt;
                &lt;label&gt;Priority&lt;/label&gt;
                &lt;select
                    name="priority"
                    value={formData.priority}
                    onChange={handleChange}
                &gt;
                    &lt;option value="low"&gt;Low&lt;/option&gt;
                    &lt;option value="medium"&gt;Medium&lt;/option&gt;
                    &lt;option value="high"&gt;High&lt;/option&gt;
                &lt;/select&gt;
            &lt;/div&gt;
        &lt;/div&gt;

        &lt;div className="form-group"&gt;
            &lt;label&gt;Due Date&lt;/label&gt;
            &lt;input
                type="date"
                name="dueDate"
                value={formData.dueDate}
                onChange={handleChange}
            /&gt;
        &lt;/div&gt;

        &lt;button type="submit" className="btn btn-primary"&gt;
            {initialData ? 'Update Task' : 'Create Task'}
        &lt;/button&gt;
    &lt;/form&gt;
);

} 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) =&gt; {
    const badges = {
        'pending': 'ā³ Pending',
        'in-progress': 'šŸ”„ In Progress',
        'completed': 'āœ… Completed'
    };
    return badges[status] || status;
};

return (
    &lt;div className="task-item"&gt;
        &lt;div className="task-header"&gt;
            &lt;h3&gt;{task.title}&lt;/h3&gt;
            &lt;span
                className="priority-badge"
                style={{ backgroundColor: getPriorityColor(task.priority) }}
            &gt;
                {task.priority}
            &lt;/span&gt;
        &lt;/div&gt;

        &lt;p className="task-description"&gt;{task.description}&lt;/p&gt;

        &lt;div className="task-meta"&gt;
            &lt;span className="status-badge"&gt;{getStatusBadge(task.status)}&lt;/span&gt;
            {task.dueDate &amp;&amp; (
                &lt;span className="due-date"&gt;
                    šŸ“… {new Date(task.dueDate).toLocaleDateString()}&lt;/span&gt;
            )}
        &lt;/div&gt;

        &lt;div className="task-actions"&gt;
            &lt;button
                onClick={() =&gt; onUpdate(task)}
                className="btn btn-edit"
            &gt;
                Edit
            &lt;/button&gt;
            &lt;button
                onClick={() =&gt; onDelete(task._id)}
                className="btn btn-delete"
            &gt;
                Delete
            &lt;/button&gt;
        &lt;/div&gt;
    &lt;/div&gt;
);

}

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(() =&gt; {
    fetchTasks();
}, []);

const fetchTasks = async () =&gt; {
    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) =&gt; {
    try {
        await taskAPI.createTask(taskData);
        fetchTasks();
    } catch (err) {
        setError('Failed to create task');
        console.error(err);
    }
};

const handleUpdateTask = async (taskData) =&gt; {
    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) =&gt; {
    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 &lt;div className="loading"&gt;Loading tasks...&lt;/div&gt;;
}

return (
    &lt;div className="App"&gt;
        &lt;header className="app-header"&gt;
            &lt;h1&gt;šŸ“ MERN Task Manager&lt;/h1&gt;
            &lt;p&gt;Manage your tasks efficiently&lt;/p&gt;
        &lt;/header&gt;

        {error &amp;&amp; (
            &lt;div className="error-message"&gt;
                {error}
                &lt;button onClick={() =&gt; setError(null)}&gt;āœ•&lt;/button&gt;
            &lt;/div&gt;
        )}

        &lt;div className="container"&gt;
            &lt;div className="form-section"&gt;
                &lt;h2&gt;{editingTask ? 'Edit Task' : 'Create New Task'}&lt;/h2&gt;
                &lt;TaskForm
                    onSubmit={editingTask ? handleUpdateTask : handleCreateTask}
                    initialData={editingTask}
                /&gt;
                {editingTask &amp;&amp; (
                    &lt;button
                        onClick={() =&gt; setEditingTask(null)}
                        className="btn btn-cancel"
                    &gt;
                        Cancel Edit
                    &lt;/button&gt;
                )}
            &lt;/div&gt;

            &lt;div className="tasks-section"&gt;
                &lt;h2&gt;Your Tasks ({tasks.length})&lt;/h2&gt;
                {tasks.length === 0 ? (
                    &lt;p className="no-tasks"&gt;No tasks yet. Create one to get started!&lt;/p&gt;
                ) : (
                    &lt;div className="tasks-grid"&gt;
                        {tasks.map(task =&gt; (
                            &lt;TaskItem
                                key={task._id}
                                task={task}
                                onDelete={handleDeleteTask}
                                onUpdate={setEditingTask}
                            /&gt;
                        ))}
                    &lt;/div&gt;
                )}
            &lt;/div&gt;
        &lt;/div&gt;
    &lt;/div&gt;
);

}

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>

Share this article