Skip to main content
Node.js Basics
CHAPTER 30 Beginner

Fullstack Node.js Project (Capstone)

Updated: May 13, 2026
45 min read

# Fullstack Node.js Project (Capstone)

Welcome to Chapter 30—the grand finale! Over the past 29 chapters, you have evolved from understanding basic JavaScript syntax to building secure, cloud-connected backend architectures.

To solidify your knowledge, you must build something from scratch. In this capstone project, we will build the backend for a Task Management Application (like Trello or Todoist).

---

1. Project Specifications

Your goal is to build a complete REST API that supports user authentication and private task management.

Features Required:

  1. 1. User Auth: Users can register and login. Passwords must be hashed via bcryptjs.
  1. 2. JWT Security: Users receive a JWT on login. They must use this token to access their tasks.
  1. 3. Task CRUD: Users can Create, Read, Update, and Delete tasks.
  1. 4. Data Isolation: User A cannot see or delete User B's tasks. (Tasks are tied to specific users).
  1. 5. Database: Connected to MongoDB Atlas via Mongoose.
  1. 6. Architecture: Use the MVC folder structure.
  1. 7. Security: Use a Global Error Handler.

---

2. Project Setup

Follow along, but try to write the logic yourself before looking at the solution!

Step 1: Initialization

bash
12345
mkdir task-manager-api
cd task-manager-api
npm init -y
npm install express mongoose dotenv bcryptjs jsonwebtoken
npm install --save-dev nodemon

Step 2: Folder Structure Create the following structure:

text
12345678910111213
/models
  User.js
  Task.js
/controllers
  authController.js
  taskController.js
/routes
  authRoutes.js
  taskRoutes.js
/middleware
  authMiddleware.js
.env
app.js

---

3. Database Models

Let's define our Mongoose Schemas.

models/User.js ```javascript id="ch30-model-user" const mongoose = require('mongoose');

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

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

12
**`models/Task.js`**
*(Crucial: We must link the task to the user who created it using `mongoose.Schema.Types.ObjectId`)*

javascript id="ch30-model-task" const mongoose = require('mongoose');

const taskSchema = new mongoose.Schema({ title: { type: String, required: true }, completed: { type: Boolean, default: false }, // Link this task to a specific User document user: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true } }, { timestamps: true });

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

1234567
---

## 4. The Auth Middleware

We need our "bouncer" to protect the task routes.

**`middleware/authMiddleware.js`**

javascript id="ch30-middleware" const jwt = require('jsonwebtoken');

module.exports = (req, res, next) => { const authHeader = req.header('Authorization'); if (!authHeader) return res.status(401).json({ error: "Access Denied" });

const token = authHeader.split(' ')[1];

try { const verified = jwt.verify(token, process.env.JWT_SECRET); req.user = verified; // Attach { userId: ... } to req next(); } catch (err) { res.status(403).json({ error: "Invalid Token" }); } };

12345
---

## 5. Auth Controller & Routes

**`controllers/authController.js`**

javascript id="ch30-auth-ctrl" const User = require('../models/User'); const bcrypt = require('bcryptjs'); const jwt = require('jsonwebtoken');

exports.register = async (req, res, next) => { try { const { username, password } = req.body; const salt = await bcrypt.genSalt(10); const hashedPassword = await bcrypt.hash(password, salt); await User.create({ username, password: hashedPassword }); res.status(201).json({ message: "User registered" }); } catch (err) { next(err); // Pass to global error handler } };

exports.login = async (req, res, next) => { try { const { username, password } = req.body; const user = await User.findOne({ username }); if (!user || !(await bcrypt.compare(password, user.password))) { return res.status(400).json({ error: "Invalid credentials" }); } const token = jwt.sign({ userId: user.id }, process.env.JWTSECRET, { expiresIn: '1d' }); res.status(200).json({ token }); } catch (err) { next(err); } };

1
**`routes/authRoutes.js`**

javascript id="ch30-auth-route" const express = require('express'); const router = express.Router(); const authController = require('../controllers/authController');

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

module.exports = router;

1234567
---

## 6. Task Controller & Routes

*This is the core logic. Notice how we use `req.user.userId` (provided by the middleware) to ensure users only interact with their own tasks!*

**`controllers/taskController.js`**

javascript id="ch30-task-ctrl" const Task = require('../models/Task');

// CREATE TASK exports.createTask = async (req, res, next) => { try { const newTask = await Task.create({ title: req.body.title, user: req.user.userId // Tie task to logged-in user }); res.status(201).json(newTask); } catch (err) { next(err); } };

// GET MY TASKS exports.getTasks = async (req, res, next) => { try { // Only find tasks where the user field matches the logged-in user const tasks = await Task.find({ user: req.user.userId }); res.status(200).json(tasks); } catch (err) { next(err); } };

// DELETE TASK exports.deleteTask = async (req, res, next) => { try { // Find task by ID AND ensure the logged-in user owns it const task = await Task.findOneAndDelete({ _id: req.params.id, user: req.user.userId }); if (!task) return res.status(404).json({ error: "Task not found or unauthorized" }); res.status(200).json({ message: "Task deleted" }); } catch (err) { next(err); } };

1
**`routes/taskRoutes.js`**

javascript id="ch30-task-route" const express = require('express'); const router = express.Router(); const taskController = require('../controllers/taskController'); const authMiddleware = require('../middleware/authMiddleware');

// ALL routes below this line are protected by the auth middleware! router.use(authMiddleware);

router.post('/', taskController.createTask); router.get('/', taskController.getTasks); router.delete('/:id', taskController.deleteTask);

module.exports = router;

1234567
---

## 7. Tying it all together (`app.js`)

Finally, we construct the main server file, load variables, connect the database, mount the routes, and add the Global Error Handler.

**`app.js`**

javascript id="ch30-app" require('dotenv').config(); const express = require('express'); const mongoose = require('mongoose');

// Import Routes const authRoutes = require('./routes/authRoutes'); const taskRoutes = require('./routes/taskRoutes');

const app = express();

// Middleware app.use(express.json());

// Database Connection mongoose.connect(process.env.MONGOURI) .then(() => console.log('✅ MongoDB Connected')) .catch(err => console.error('❌ DB Error:', err));

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

// Catch-all route app.use('*', (req, res) => res.status(404).json({ error: "Route not found" }));

// GLOBAL ERROR HANDLER app.use((err, req, res, next) => { console.error(err); res.status(500).json({ error: err.message || "Internal Server Error" }); });

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

---

8. How to Test Your App

To test this API, you must use Postman or Insomnia:

  1. 1. Make a POST request to /api/auth/register with a JSON body (username, password).
  1. 2. Make a POST request to /api/auth/login. Copy the JWT string from the response.
  1. 3. Make a POST request to /api/tasks. In Postman, go to the Headers tab, add Authorization as the key, and Bearer <yourcopied_token> as the value. Add a title to the body and send!
  1. 4. Make a GET request to /api/tasks using the same header to see your task.

---

9. Conclusion

Congratulations! 🎉

You have officially completed the Node.js Basics Curriculum. You have journeyed from understanding the V8 engine and the fs` module to building a complete, secure, MVC-architected API.

What's Next?

  • Frontend Integration: Learn React or Vue, and build a beautiful UI that fetches data from this very API you just built!
  • Advanced Node.js: Explore WebSockets (Socket.io) for real-time chat apps, GraphQL, or Microservices architecture.

The backend world is now at your fingertips. Happy Coding!

Finish this Chapter

Save your progress on your learning path and prepare for coding interview challenges.

Discussion

Join the discussion

Log in or create a free account to participate.

Sort: ·