MERN Stack Tutorial: Build a Full-Stack App from Scratch
โก Quick Answer
A complete MERN stack tutorial โ build a full-stack app with MongoDB, Express, React, and Node.js from scratch, including authentication, REST API, and deployment.
Get more content like this on Telegram!
Daily AI tips, notes & resources โ free
Advertisement
MERN Stack Tutorial: Build a Full-Stack App from Scratch
MERN stands for MongoDB, Express.js, React, and Node.js โ a full-stack JavaScript combination where the same language runs the database queries, the backend server, and the frontend UI.
The first time I tried to understand a full-stack application, I felt like I was trying to see all four floors of a building at once while standing inside it. Which layer handles authentication? How does data flow from the React component to the database and back? Where do environment variables live?
MERN makes sense as a stack because JavaScript flows through every layer โ the same language that runs in the browser also runs the server and structures the data. Once you see one complete request cycle from React component to MongoDB and back, the architecture clicks.
This tutorial builds a task management API with complete CRUD operations, JWT authentication, and a working React frontend โ everything connected and deployed by the end.
Project Architecture Overview
We're building a Task Manager with:
- MongoDB Atlas: Cloud database (tasks and users)
- Express.js + Node.js: REST API backend (authentication, CRUD endpoints)
- React + Vite: Frontend (task list UI, forms, auth flow)
Part 1: Backend Setup
Initialize the Project
mkdir mern-tasks && cd mern-tasks
mkdir server && cd server
npm init -y
npm install express mongoose dotenv bcryptjs jsonwebtoken cors express-validator
npm install --save-dev nodemonProject Structure
server/
โโโ models/
โ โโโ User.js
โ โโโ Task.js
โโโ routes/
โ โโโ auth.js
โ โโโ tasks.js
โโโ middleware/
โ โโโ auth.js
โโโ .env
โโโ server.jsserver.js โ Express Application
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
require('dotenv').config();
const app = express();
// Middleware
app.use(cors({ origin: process.env.CLIENT_URL || 'http://localhost:5173' }));
app.use(express.json());
// Routes
app.use('/api/auth', require('./routes/auth'));
app.use('/api/tasks', require('./routes/tasks'));
// Connect to MongoDB
mongoose.connect(process.env.MONGODB_URI)
.then(() => console.log('MongoDB connected'))
.catch(err => console.error('MongoDB connection error:', err));
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));.env File
MONGODB_URI=mongodb+srv://your-connection-string
JWT_SECRET=your-jwt-secret-min-32-chars
PORT=5000
CLIENT_URL=http://localhost:5173Mongoose Models
// models/User.js
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const userSchema = new mongoose.Schema({
name: { type: String, required: true, trim: true },
email: {
type: String,
required: true,
unique: true,
lowercase: true,
trim: true,
},
password: { type: String, required: true, minlength: 6 },
}, { timestamps: true });
// Hash password before saving
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
this.password = await bcrypt.hash(this.password, 10);
next();
});
// Compare password method
userSchema.methods.comparePassword = function(candidatePassword) {
return bcrypt.compare(candidatePassword, this.password);
};
module.exports = mongoose.model('User', userSchema);// models/Task.js
const mongoose = require('mongoose');
const taskSchema = new mongoose.Schema({
title: { type: String, required: true, trim: true },
description: { type: String, trim: true },
completed: { type: Boolean, default: false },
priority: {
type: String,
enum: ['low', 'medium', 'high'],
default: 'medium',
},
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true,
},
}, { timestamps: true });
module.exports = mongoose.model('Task', taskSchema);Authentication Middleware
// middleware/auth.js
const jwt = require('jsonwebtoken');
module.exports = (req, res, next) => {
const token = req.header('Authorization')?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ error: 'Authentication required' });
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.userId = decoded.userId;
next();
} catch {
res.status(401).json({ error: 'Invalid or expired token' });
}
};Auth Routes
// routes/auth.js
const router = require('express').Router();
const jwt = require('jsonwebtoken');
const User = require('../models/User');
// POST /api/auth/register
router.post('/register', async (req, res) => {
try {
const { name, email, password } = req.body;
const existingUser = await User.findOne({ email });
if (existingUser) {
return res.status(409).json({ error: 'Email already in use' });
}
const user = new User({ name, email, password });
await user.save();
const token = jwt.sign({ userId: user._id }, process.env.JWT_SECRET, {
expiresIn: '7d',
});
res.status(201).json({
token,
user: { id: user._id, name: user.name, email: user.email },
});
} catch (error) {
res.status(500).json({ error: 'Server error' });
}
});
// POST /api/auth/login
router.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
const user = await User.findOne({ email });
if (!user || !(await user.comparePassword(password))) {
return res.status(401).json({ error: 'Invalid email or password' });
}
const token = jwt.sign({ userId: user._id }, process.env.JWT_SECRET, {
expiresIn: '7d',
});
res.json({
token,
user: { id: user._id, name: user.name, email: user.email },
});
} catch (error) {
res.status(500).json({ error: 'Server error' });
}
});
module.exports = router;Tasks Routes
// routes/tasks.js
const router = require('express').Router();
const auth = require('../middleware/auth');
const Task = require('../models/Task');
// All task routes require authentication
router.use(auth);
// GET /api/tasks โ Get all tasks for authenticated user
router.get('/', async (req, res) => {
try {
const tasks = await Task.find({ user: req.userId })
.sort({ createdAt: -1 });
res.json(tasks);
} catch {
res.status(500).json({ error: 'Server error' });
}
});
// POST /api/tasks โ Create new task
router.post('/', async (req, res) => {
try {
const task = new Task({ ...req.body, user: req.userId });
await task.save();
res.status(201).json(task);
} catch (error) {
res.status(400).json({ error: error.message });
}
});
// PATCH /api/tasks/:id โ Update task
router.patch('/:id', async (req, res) => {
try {
const task = await Task.findOneAndUpdate(
{ _id: req.params.id, user: req.userId },
req.body,
{ new: true, runValidators: true }
);
if (!task) return res.status(404).json({ error: 'Task not found' });
res.json(task);
} catch {
res.status(500).json({ error: 'Server error' });
}
});
// DELETE /api/tasks/:id โ Delete task
router.delete('/:id', async (req, res) => {
try {
const task = await Task.findOneAndDelete({
_id: req.params.id,
user: req.userId,
});
if (!task) return res.status(404).json({ error: 'Task not found' });
res.json({ message: 'Task deleted' });
} catch {
res.status(500).json({ error: 'Server error' });
}
});
module.exports = router;Part 2: React Frontend
cd ..
npm create vite@latest client -- --template react
cd client
npm install axios react-router-domKey React Concepts
Authentication State Management with Context:
// context/AuthContext.jsx
import { createContext, useContext, useState } from 'react';
import axios from 'axios';
const AuthContext = createContext();
export function AuthProvider({ children }) {
const [user, setUser] = useState(
() => JSON.parse(localStorage.getItem('user'))
);
const token = localStorage.getItem('token');
if (token) {
axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
}
const login = async (email, password) => {
const { data } = await axios.post('/api/auth/login', { email, password });
localStorage.setItem('token', data.token);
localStorage.setItem('user', JSON.stringify(data.user));
axios.defaults.headers.common['Authorization'] = `Bearer ${data.token}`;
setUser(data.user);
};
const logout = () => {
localStorage.removeItem('token');
localStorage.removeItem('user');
delete axios.defaults.headers.common['Authorization'];
setUser(null);
};
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
}
export const useAuth = () => useContext(AuthContext);Deployment
MongoDB Atlas: Create free cluster at mongodb.com/atlas. Get connection string.
Backend (Railway):
- Push code to GitHub
- Connect Railway to GitHub repo
- Set environment variables (MONGODB_URI, JWT_SECRET, CLIENT_URL)
- Deploy
Frontend (Vercel):
- Set
VITE_API_URLto your Railway URL - Connect Vercel to GitHub frontend repo
- Deploy automatically
For deeper SQL database understanding alongside MongoDB, see our SQL guide and the full full-stack roadmap.
Further Reading
- Building a SaaS App with Next.js, Supabase, and Stripe
- PostgreSQL vs MongoDB: Which Database for Your Next Project?
- Microservices vs Monolith in 2025: Which Architecture to Choose
- How to Get Your First Full-Stack Job Without a CS Degree
- The Full Stack Developer Salary Guide for 2025 by Country
- Jupyter Notebook Guide: The Data Scientist's Favorite Tool
- FastAPI Tutorial: Building Your First REST API in 30 Minutes
- How Python Is Used in Finance: Real Wall Street Use Cases
Advertisement
๐ฌ DiscussionPowered by GitHub Discussions
Frequently Asked Questions
Problem Solver and Cloud Expert
Solves complex infrastructure challenges and architects reliable, scalable cloud deployments for every project. Shamshur Rahman keeps AiTechWorldsโ hosting and cloud infrastructure fast, resilient, and cost-efficient.
Not sure yet? Ask AI about this article
Get an instant, unbiased AI summary of โMERN Stack Tutorial: Build a Full-Stack App from Scratchโ.
Advertisement
Related Articles
How I Built a Full-Stack App in 48 Hours Using AI Tools
Learn how to use AI tools to build a full-stack app fast โ GitHub Copilot, Claude, and ChatGPT for planning, coding, debugging, and deploying a real web application in 48 hours.
The 2026 Full Stack Developer Roadmap: From Zero to Job-Ready
The complete full stack developer roadmap for 2026 โ learn frontend, backend, databases, DevOps, and the exact learning path from beginner to job-ready in 12โ18 months.
The Full Stack Developer Salary Guide for 2026 by Country
Full stack developer salary guide 2026 โ average salaries by country, experience level, tech stack, and remote work, plus tips to negotiate a higher salary.
How to Get Your First Full-Stack Job Without a CS Degree
Full stack job no degree guide โ how self-taught developers and bootcamp grads land their first software job with a portfolio, networking, and interview prep.