Project 5: Full-Stack App with Node.js & Express
📖 Concept
Build a complete full-stack application with a Node.js/Express backend and a JavaScript frontend. This capstone project combines everything you've learned.
Architecture:
- Backend: Node.js + Express REST API
- Database: MongoDB/PostgreSQL (or JSON file for simplicity)
- Frontend: Vanilla JavaScript SPA
- Auth: JWT-based authentication
Features:
- User registration and login (JWT auth)
- CRUD operations for a resource (posts, products, etc.)
- Input validation and error handling
- Pagination, search, and filtering
- File upload capability
Concepts practiced: Node.js, Express, REST API design, JWT, middleware, fetch, async/await, MVC pattern, error handling, security
💻 Code Example
1// ===== Express Backend =====23// server.js4const express = require("express");5const jwt = require("jsonwebtoken");6const app = express();78app.use(express.json());910// In-memory store (use a database in production)11let users = [];12let posts = [];13const JWT_SECRET = "your-secret-key";1415// Auth middleware16function authenticate(req, res, next) {17 const token = req.headers.authorization?.split(" ")[1];18 if (!token) return res.status(401).json({ error: "No token" });19 try {20 req.user = jwt.verify(token, JWT_SECRET);21 next();22 } catch {23 res.status(401).json({ error: "Invalid token" });24 }25}2627// Auth routes28app.post("/api/register", (req, res) => {29 const { email, password, name } = req.body;30 if (!email || !password) {31 return res.status(400).json({ error: "Email and password required" });32 }33 if (users.find(u => u.email === email)) {34 return res.status(409).json({ error: "Email already exists" });35 }36 const user = { id: Date.now().toString(), email, name };37 users.push({ ...user, password }); // Hash in production!38 const token = jwt.sign(user, JWT_SECRET, { expiresIn: "7d" });39 res.status(201).json({ user, token });40});4142app.post("/api/login", (req, res) => {43 const { email, password } = req.body;44 const user = users.find(u => u.email === email && u.password === password);45 if (!user) return res.status(401).json({ error: "Invalid credentials" });46 const { password: _, ...safeUser } = user;47 const token = jwt.sign(safeUser, JWT_SECRET, { expiresIn: "7d" });48 res.json({ user: safeUser, token });49});5051// CRUD routes (protected)52app.get("/api/posts", authenticate, (req, res) => {53 const { page = 1, limit = 10, search = "" } = req.query;54 let filtered = posts.filter(p =>55 p.title.toLowerCase().includes(search.toLowerCase())56 );57 const total = filtered.length;58 const paginated = filtered.slice((page - 1) * limit, page * limit);59 res.json({ posts: paginated, total, page: +page, totalPages: Math.ceil(total / limit) });60});6162app.post("/api/posts", authenticate, (req, res) => {63 const post = {64 id: Date.now().toString(),65 ...req.body,66 author: req.user.id,67 createdAt: new Date().toISOString()68 };69 posts.unshift(post);70 res.status(201).json(post);71});7273app.put("/api/posts/:id", authenticate, (req, res) => {74 const index = posts.findIndex(p => p.id === req.params.id);75 if (index === -1) return res.status(404).json({ error: "Not found" });76 if (posts[index].author !== req.user.id) {77 return res.status(403).json({ error: "Forbidden" });78 }79 posts[index] = { ...posts[index], ...req.body, updatedAt: new Date().toISOString() };80 res.json(posts[index]);81});8283app.delete("/api/posts/:id", authenticate, (req, res) => {84 posts = posts.filter(p => p.id !== req.params.id);85 res.status(204).end();86});8788app.listen(3000, () => console.log("Server running on port 3000"));8990// ===== Frontend API Client =====91class ApiClient {92 constructor(baseURL) {93 this.baseURL = baseURL;94 this.token = localStorage.getItem("token");95 }9697 async request(endpoint, options = {}) {98 const res = await fetch(`${this.baseURL}${endpoint}`, {99 ...options,100 headers: {101 "Content-Type": "application/json",102 ...(this.token && { Authorization: `Bearer ${this.token}` }),103 ...options.headers104 }105 });106 if (!res.ok) {107 const error = await res.json().catch(() => ({}));108 throw new Error(error.message || `HTTP ${res.status}`);109 }110 return res.status === 204 ? null : res.json();111 }112113 // Auth114 async login(email, password) {115 const { user, token } = await this.request("/api/login", {116 method: "POST", body: JSON.stringify({ email, password })117 });118 this.token = token;119 localStorage.setItem("token", token);120 return user;121 }122123 // CRUD124 getPosts(params) { return this.request(`/api/posts?${new URLSearchParams(params)}`); }125 createPost(data) { return this.request("/api/posts", { method: "POST", body: JSON.stringify(data) }); }126 updatePost(id, data) { return this.request(`/api/posts/${id}`, { method: "PUT", body: JSON.stringify(data) }); }127 deletePost(id) { return this.request(`/api/posts/${id}`, { method: "DELETE" }); }128}
🏋️ Practice Exercise
Build It Yourself:
- Set up Express server with the routes above
- Add password hashing with bcrypt
- Create a frontend SPA with login, registration, and CRUD UI
- Implement pagination and search on the frontend
- Add form validation on both client and server
- Implement proper error handling and toast notifications
- Bonus: Add file uploads, comments, and user profiles
⚠️ Common Mistakes
Storing passwords in plain text — always use bcrypt or argon2
Not validating input on the server — client-side validation can be bypassed
Storing JWT in localStorage (XSS risk) — use HttpOnly cookies for production
Not handling token expiration — redirect to login when 401 is received
CORS issues — configure CORS middleware properly for your frontend origin
💼 Interview Questions
🎤 Mock Interview
Mock interview is powered by AI for Project 5: Full-Stack App with Node.js & Express. Login to unlock this feature.