Fullstack Node.js Project (Capstone)
# 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.
User Auth: Users can register and login. Passwords must be hashed via
bcryptjs.
- 2. JWT Security: Users receive a JWT on login. They must use this token to access their tasks.
- 3. Task CRUD: Users can Create, Read, Update, and Delete tasks.
- 4. Data Isolation: User A cannot see or delete User B's tasks. (Tasks are tied to specific users).
- 5. Database: Connected to MongoDB Atlas via Mongoose.
- 6. Architecture: Use the MVC folder structure.
- 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
Step 2: Folder Structure Create the following structure:
---
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);
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);
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" }); } };
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); } };
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;
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); } };
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;
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.
Make a POST
request to/api/auth/registerwith a JSON body (username,password).
-
2.
Make a POST
request to/api/auth/login. Copy the JWT string from the response.
-
3.
Make a POST
request to/api/tasks. In Postman, go to the Headers tab, addAuthorizationas the key, andBearer <yourcopied_token>as the value. Add atitleto the body and send!
-
4.
Make a GET
request to/api/tasksusing 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!