In 2025, building fullstack applications is easier, faster, and more scalable than ever. Thanks to the powerful trio of Node.js, Express.js, and MongoDB, you can build modern web applications with speed and efficiency.

Whether you're creating a blog, SaaS tool, or e-commerce store, this tech stack provides the flexibility and power you need.


πŸ”§ What You’ll Build

For this tutorial, we’ll build a simple "Task Manager" app where users can:

  • Register/Login

  • Create, Read, Update, and Delete (CRUD) tasks

  • View their tasks on a dashboard


πŸš€ Tools & Tech Stack

  • Node.js – JavaScript runtime for backend

  • Express.js – Lightweight web framework for Node.js

  • MongoDB – NoSQL database

  • Mongoose – ODM for MongoDB

  • JWT – For authentication

  • Tailwind CSS or basic HTML for the frontend

  • Postman – For testing APIs

  • Vite/React (optional) – If you want a modern frontend


πŸ—‚οΈ Folder Structure

task-manager/
β”‚
β”œβ”€β”€ backend/
β”‚   β”œβ”€β”€ config/
β”‚   β”œβ”€β”€ controllers/
β”‚   β”œβ”€β”€ models/
β”‚   β”œβ”€β”€ routes/
β”‚   β”œβ”€β”€ middleware/
β”‚   β”œβ”€β”€ server.js
β”‚   └── .env
β”‚
β”œβ”€β”€ frontend/ (optional React/Vite setup)
β”‚
└── README.md

1. βœ… Initialize Your Project

mkdir task-manager
cd task-manager
mkdir backend && cd backend
npm init -y

Install dependencies:

npm install express mongoose dotenv cors jsonwebtoken bcryptjs
npm install --save-dev nodemon

Add this to package.json for hot reloading:

"scripts": {
  "dev": "nodemon server.js"
}

2. 🌐 Setup Express Server

backend/server.js

const express = require('express');
const mongoose = require('mongoose');
const dotenv = require('dotenv');
const cors = require('cors');

dotenv.config();
const app = express();

app.use(cors());
app.use(express.json());

// Routes
app.use('/api/auth', require('./routes/authRoutes'));
app.use('/api/tasks', require('./routes/taskRoutes'));

// MongoDB Connection
mongoose.connect(process.env.MONGO_URI)
  .then(() => console.log('MongoDB Connected'))
  .catch((err) => console.error(err));

const PORT = process.env.PORT || 5000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));

3. 🧱 Create User & Task Models

models/User.js

const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');

const userSchema = new mongoose.Schema({
  name: { type: String, required: true },
  email: { type: String, unique: true },
  password: { type: String, required: true }
});

// Encrypt password before saving
userSchema.pre('save', async function () {
  if (!this.isModified('password')) return;
  this.password = await bcrypt.hash(this.password, 12);
});

module.exports = mongoose.model('User', userSchema);

models/Task.js

const mongoose = require('mongoose');

const taskSchema = new mongoose.Schema({
  user: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
  title: String,
  completed: { type: Boolean, default: false },
}, { timestamps: true });

module.exports = mongoose.model('Task', taskSchema);

4. πŸ” Authentication with JWT

routes/authRoutes.js

const express = require('express');
const { register, login } = require('../controllers/authController');
const router = express.Router();

router.post('/register', register);
router.post('/login', login);

module.exports = router;

controllers/authController.js

const User = require('../models/User');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');

const generateToken = (id) => jwt.sign({ id }, process.env.JWT_SECRET, { expiresIn: '7d' });

exports.register = async (req, res) => {
  const { name, email, password } = req.body;
  const userExists = await User.findOne({ email });
  if (userExists) return res.status(400).json({ msg: 'User already exists' });

  const user = await User.create({ name, email, password });
  const token = generateToken(user._id);
  res.status(201).json({ token, user: { id: user._id, name: user.name } });
};

exports.login = async (req, res) => {
  const { email, password } = req.body;
  const user = await User.findOne({ email });
  if (!user || !(await bcrypt.compare(password, user.password))) {
    return res.status(401).json({ msg: 'Invalid credentials' });
  }
  const token = generateToken(user._id);
  res.json({ token, user: { id: user._id, name: user.name } });
};

5. πŸ›‘οΈ Middleware: Protect Routes

middleware/authMiddleware.js

const jwt = require('jsonwebtoken');
const User = require('../models/User');

const protect = async (req, res, next) => {
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) return res.status(401).json({ msg: 'Not authorized' });

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = await User.findById(decoded.id).select('-password');
    next();
  } catch {
    res.status(401).json({ msg: 'Token failed' });
  }
};

module.exports = protect;

6. πŸ“‹ Task Routes & Controllers

routes/taskRoutes.js

const express = require('express');
const protect = require('../middleware/authMiddleware');
const { getTasks, addTask, updateTask, deleteTask } = require('../controllers/taskController');
const router = express.Router();

router.route('/').get(protect, getTasks).post(protect, addTask);
router.route('/:id').put(protect, updateTask).delete(protect, deleteTask);

module.exports = router;

controllers/taskController.js

const Task = require('../models/Task');

exports.getTasks = async (req, res) => {
  const tasks = await Task.find({ user: req.user._id });
  res.json(tasks);
};

exports.addTask = async (req, res) => {
  const task = await Task.create({ ...req.body, user: req.user._id });
  res.status(201).json(task);
};

exports.updateTask = async (req, res) => {
  const task = await Task.findById(req.params.id);
  if (task.user.toString() !== req.user._id.toString()) return res.status(401).send("Unauthorized");
  const updated = await Task.findByIdAndUpdate(req.params.id, req.body, { new: true });
  res.json(updated);
};

exports.deleteTask = async (req, res) => {
  const task = await Task.findById(req.params.id);
  if (task.user.toString() !== req.user._id.toString()) return res.status(401).send("Unauthorized");
  await task.deleteOne();
  res.json({ msg: 'Task removed' });
};

7. πŸ§ͺ Testing APIs with Postman

Use Postman to:

  • Register a new user (POST /api/auth/register)

  • Login (POST /api/auth/login)

  • Get JWT and use it in Headers as:
    Authorization: Bearer <token>

  • Create, fetch, update, and delete tasks via respective endpoints


8. 🎨 Add a Frontend (Optional)

You can use React + Vite or any frontend stack:

  • Fetch tasks from /api/tasks

  • Use Axios or Fetch API to handle login/register

  • Store token in localStorage or Cookies

  • Protect frontend routes using the token


πŸ“¦ Deployment

  • Backend: Deploy on Render, Railway, or Vercel

  • Database: MongoDB Atlas

  • Frontend: Vercel or Netlify

Make sure to set environment variables like MONGO_URI and JWT_SECRET.


🧠 Final Thoughts

In 2025, building fullstack applications with Node.js, Express, and MongoDB is still an excellent and in-demand approach. With rapid development, RESTful APIs, and easy deployment options, this stack empowers you to build scalable, real-world apps quickly.