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

codeTap to expand ⛶
1// ===== Express Backend =====
2
3// server.js
4const express = require("express");
5const jwt = require("jsonwebtoken");
6const app = express();
7
8app.use(express.json());
9
10// In-memory store (use a database in production)
11let users = [];
12let posts = [];
13const JWT_SECRET = "your-secret-key";
14
15// Auth middleware
16function 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}
26
27// Auth routes
28app.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});
41
42app.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});
50
51// 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});
61
62app.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});
72
73app.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});
82
83app.delete("/api/posts/:id", authenticate, (req, res) => {
84 posts = posts.filter(p => p.id !== req.params.id);
85 res.status(204).end();
86});
87
88app.listen(3000, () => console.log("Server running on port 3000"));
89
90// ===== Frontend API Client =====
91class ApiClient {
92 constructor(baseURL) {
93 this.baseURL = baseURL;
94 this.token = localStorage.getItem("token");
95 }
96
97 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.headers
104 }
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 }
112
113 // Auth
114 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 }
122
123 // CRUD
124 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:

  1. Set up Express server with the routes above
  2. Add password hashing with bcrypt
  3. Create a frontend SPA with login, registration, and CRUD UI
  4. Implement pagination and search on the frontend
  5. Add form validation on both client and server
  6. Implement proper error handling and toast notifications
  7. 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.