Follow AiTechWorlds on LinkedIn for professional AI content!Follow Now →

MERN Stack Tutorial: Build a Full-Stack App from Scratch

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.

A
AiTechWorlds Team
May 27, 2026 7 min read
📱

Get more content like this on Telegram!

Daily AI tips, notes & resources — free

Join Free →

MERN Stack Tutorial: Build a Full-Stack App from Scratch

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)
Client (React)          Server (Node/Express)       Database (MongoDB)
──────────────          ──────────────────────       ─────────────────
POST /api/auth/login ──→ Validate credentials    ──→ Find user by email
                    ←── JWT token               ←── User document

GET /api/tasks ────────→ Verify JWT token      ──→ Query tasks by userId
               ←──────── Tasks array           ←── Task documents

POST /api/tasks ───────→ Validate input        ──→ Insert new task
                ←──────── New task object      ←── Created document

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 nodemon

Project Structure

server/
├── models/
│   ├── User.js
│   └── Task.js
├── routes/
│   ├── auth.js
│   └── tasks.js
├── middleware/
│   └── auth.js
├── .env
└── server.js

server.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:5173

Mongoose 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-dom

Key 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):

  1. Push code to GitHub
  2. Connect Railway to GitHub repo
  3. Set environment variables (MONGODB_URI, JWT_SECRET, CLIENT_URL)
  4. Deploy

Frontend (Vercel):

  1. Set VITE_API_URL to your Railway URL
  2. Connect Vercel to GitHub frontend repo
  3. Deploy automatically

For deeper SQL database understanding alongside MongoDB, see our SQL guide and the full full-stack roadmap.


Frequently Asked Questions

What is the MERN stack and why is it popular?

MongoDB, Express, React, Node.js. Popular because all components use JavaScript — one language across the entire stack. Large community, npm ecosystem, and used at many coding bootcamps.

Is MERN good for beginners?

If you know JavaScript and React, the backend pieces are learnable in weeks. The architecture can be confusing at first. Following a structured tutorial provides the necessary framework.

Should I use MERN or PERN?

For most apps, PERN (PostgreSQL instead of MongoDB) is stronger. PostgreSQL handles relational data and complex queries better. Use MongoDB when your data model is truly flexible or changes frequently.

What is the difference between Express and Next.js?

Express is a backend framework for Node.js. Next.js is a React framework that includes API routes — it can replace both Express and standalone React in one framework.

How do I deploy a MERN app?

MongoDB Atlas (database) + Railway or Render (backend) + Vercel or Netlify (frontend). All have free tiers. Setup takes under an hour after your first deployment.

Share this article:

Frequently Asked Questions

MERN is MongoDB (database), Express.js (backend framework), React (frontend library), and Node.js (runtime environment). It's popular because all four components use JavaScript — one language across the entire stack. Full-stack JavaScript means developers can work on frontend and backend without language-switching, JSON flows naturally between all layers, and the npm ecosystem provides packages for virtually every need. MERN is also the stack taught at many coding bootcamps, creating a large community and abundant learning resources.
A

AiTechWorlds Team

✓ Verified Writer

The AiTechWorlds team is passionate about AI, technology, and education. We create high-quality, research-backed content to help you learn, grow, and succeed in the modern digital world.

Related Articles

10K+ Members Growing Daily

Get Free AI Notes Daily

Join AiTechWorlds on Telegram and get daily AI tips, prompt engineering templates, coding resources, and exclusive content — 100% free!

📚 Free Study Notes🤖 AI Tips Daily⚡ Prompt Templates💻 Coding Resources
Join Free Channel

No spam. Leave anytime.

!