Node.js & Express Backend Development
Building REST APIs for Full-Stack Applications
Course lectures and practices for JavaScript full‑stack web development with AI‑assisted workflows.
🗺️ The Backend Journey
EXPRESS FOUNDATIONS DATABASE & PRODUCTION
┌─────────────────┐ ┌─────────────────┐
│ Why Node/Express│ │ MySQL Setup │
│ REST API Design │ → │ Data Persistence│
│ Routes & Middle │ │ Error Handling │
│ JSON Responses │ │ Production Ready│
└─────────────────┘ └─────────────────┘
API Foundation Database Ready
💡 Why Node.js + Express?
The Full-Stack JavaScript Advantage
Traditional Approach
Frontend: JavaScript (React)
Backend: PHP/Python/Java/C#
Database: MySQL/PostgreSQL
Problems:
❌ Different languages
❌ Context switching
❌ Different deployment
❌ Different tools/debugging
Node.js Approach
Frontend: JavaScript (React)
Backend: JavaScript (Node.js)
Database: MySQL/PostgreSQL
Benefits:
- ✅ Data persistence
- ✅ ACID transactions
- ✅ Concurrent access
- ✅ Data relationships
- ✅ Backup/recovery
- ✅ Scalable queries
Real-World Success Stories
- Netflix: Migrated from Java to Node.js → 70% reduction in startup time
- Uber: Real-time matching system built on Node.js
- LinkedIn: Mobile API backend switched to Node.js → 10x performance
- PayPal: 2x faster development, 33% fewer lines of code
Why Express? The minimal, unopinionated web framework that powers 60%+ of Node.js web applications.
🎯 Bootstrap: Creating an Express Server from Scratch
Let’s create a minimal Express server step by step
Step 1: Initialize Project
# Create project directory
mkdir my-express-api
cd my-express-api
# Initialize npm project (creates package.json)
npm init -y
# Install Express
npm install expressStep 2: Create Your First Server (server.js)
// server.js - The simplest Express server
const express = require('express');
const app = express();
const PORT = 3000;
// Middleware to parse JSON
app.use(express.json());
// Your first endpoint!
app.get('/', (req, res) => {
res.json({ message: 'Hello from Express!' });
});
// Start the server
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});Step 3: Run It!
node server.js
# Visit http://localhost:3000 in your browser
# Or test with: curl http://localhost:3000In 3 steps, you have a working web server! 🎉
Teaching Points:
Step 1 - Project Setup: Emphasize that
npm init -ycreates package.json (project manifest). The-yflag accepts all defaults. This is where dependencies are tracked.Step 2 - Understanding the Code:
require('express'): Imports the Express libraryexpress(): Creates an Express application instanceapp.use(express.json()): Middleware that parses incoming JSON - CRITICAL for POST requestsapp.get('/'): Defines a route handler for GET requests to root path(req, res): Request and response objects - the heart of Expressres.json(): Sends JSON response with proper Content-Type headerapp.listen(): Starts the HTTP server on specified port
Common Student Mistakes:
- Forgetting
npm installbefore running - Not using
express.json()then wondering why POST data is undefined - Port already in use errors - teach
lsof -ti:3000 | xargs kill
- Forgetting
Live Demo Suggestion: Open two terminals - one running the server, one testing with curl. Show that the server keeps running and can handle multiple requests.
Discussion Question: “Why do we need a web server? Couldn’t we just open HTML files?” → Leads to discussion of dynamic content, databases, authentication, etc.
🧩 Understanding Endpoints
What is an Endpoint?
An endpoint is a specific URL path in your API that responds to HTTP requests. Think of it as a “door” to a specific function in your application.
Anatomy of an Endpoint:
app.METHOD(PATH, HANDLER)
│ │ │
│ │ └─ Function that handles the request
│ └──────── URL path (the "address")
└─────────────── HTTP method (GET, POST, PUT, DELETE)Example Breakdown
// GET endpoint - Retrieve data
app.get('/api/users', (req, res) => {
// ↑ ↑ ↑ ↑
// │ │ │ └─ response object (to send back)
// │ │ └────── request object (incoming data)
// │ └────────────────── The PATH (URL)
// └──────────────────────── HTTP METHOD
res.json({ users: ['Alice', 'Bob'] });
// ↑ ↑
// │ └─ Data sent back to client
// └─────── Send JSON response
});Request → Endpoint → Response
Client Server
│ │
│ GET /api/users │
│ ─────────────────────→ │
│ │ [Handler function runs]
│ │
│ { users: [...] } │
│ ←───────────────────── │
│ │
Teaching Points:
- The “Door” Analogy:
- Building = Your server
- Doors = Endpoints (each door leads to a different room/function)
- Door number = URL path
- How you open it = HTTP method (GET, POST, etc.)
- What you get inside = Response data
- HTTP Methods Quick Reference:
- GET: “Give me something” (retrieve data, safe, can be cached)
- POST: “Create something new” (submit data, not idempotent)
- PUT: “Replace this entirely” (update, idempotent)
- DELETE: “Remove this” (delete, idempotent)
- Idempotent means: calling it multiple times has the same effect as calling it once
- Request & Response Objects Deep Dive:
req.params: URL parameters (e.g.,/users/:id)req.query: Query strings (e.g.,/users?age=25)req.body: Data sent in POST/PUT requestsreq.headers: HTTP headersres.json(): Send JSON responseres.status(): Set HTTP status coderes.send(): Send plain text/HTML
- Common Student Confusion:
- “Why do we need different methods? Can’t we just use GET for everything?” → Security (GET shouldn’t modify data), semantics (REST conventions), caching behavior
- “What’s the difference between
res.json()andres.send()?” →json()sets Content-Type to application/json and stringifies the object
- Live Demo Suggestion:
- Use browser DevTools Network tab to show actual HTTP requests
- Show that GET requests appear in browser history/bookmarks, POST don’t
- Demonstrate that refreshing a GET is safe, refreshing a POST shows a warning
Check for Understanding: “If I want to update a user’s email address, which HTTP method should I use?” (PUT or PATCH)
🔍 Endpoint Example: A Complete User Service
Let’s build a complete mini-service for managing users
const express = require('express');
const app = express();
app.use(express.json()); // Parse JSON bodies
// In-memory database (for learning)
let users = [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' }
];
let nextId = 3;
// ════════════════════════════════════════════════
// ENDPOINT 1: GET all users
// ════════════════════════════════════════════════
app.get('/api/users', (req, res) => {
res.json({
success: true,
count: users.length,
data: users
});
});
// ════════════════════════════════════════════════
// ENDPOINT 2: GET single user by ID
// ════════════════════════════════════════════════
app.get('/api/users/:id', (req, res) => {
const id = parseInt(req.params.id); // URL parameter
const user = users.find(u => u.id === id);
if (!user) {
return res.status(404).json({
success: false,
error: 'User not found'
});
}
res.json({
success: true,
data: user
});
});
// ════════════════════════════════════════════════
// ENDPOINT 3: POST create new user
// ════════════════════════════════════════════════
app.post('/api/users', (req, res) => {
const { name, email } = req.body; // Request body data
// Validation
if (!name || !email) {
return res.status(400).json({
success: false,
error: 'Name and email are required'
});
}
const newUser = {
id: nextId++,
name,
email
};
users.push(newUser);
res.status(201).json({
success: true,
message: 'User created',
data: newUser
});
});
// ════════════════════════════════════════════════
// ENDPOINT 4: DELETE user
// ════════════════════════════════════════════════
app.delete('/api/users/:id', (req, res) => {
const id = parseInt(req.params.id);
const index = users.findIndex(u => u.id === id);
if (index === -1) {
return res.status(404).json({
success: false,
error: 'User not found'
});
}
users.splice(index, 1);
res.json({
success: true,
message: 'User deleted'
});
});
app.listen(3000, () => {
console.log('User service running on http://localhost:3000');
});Teaching Points:
Why In-Memory Storage?
- Perfect for learning - no database setup needed
- Shows the logic without database complexity
- Students understand it’s temporary (resets on server restart)
- Later we’ll replace with MySQL for persistence
Code Walkthrough - Key Patterns:
Pattern 1: Consistent Response Format
// Always include success flag and structured data { success: true, data: {...} } // Success { success: false, error: "..." } // ErrorThis makes client-side handling predictable!
Pattern 2: URL Parameters
app.get('/api/users/:id', ...) // Define with :id req.params.id // Access in handlerThe
:idis a variable placeholder./api/users/42→req.params.id = "42"Pattern 3: Request Body
app.use(express.json()) // MUST have this middleware! req.body.name // Access POST dataWithout
express.json(),req.bodyis undefined!Pattern 4: HTTP Status Codes
200: OK (default for successful GET/PUT/DELETE)201: Created (for successful POST)400: Bad Request (client error - validation failed)404: Not Found (resource doesn’t exist)500: Internal Server Error (server error)
Validation is CRITICAL:
- Never trust client input
- Check required fields exist
- Validate data types (parseInt for IDs)
- Return early with error response (notice the
return)
Common Student Mistakes:
- Forgetting
parseInt()on req.params.id → string comparison fails - Not using
returnwith error responses → code continues, sends multiple responses! - Forgetting to call
express.json()→ req.body is undefined - Using wrong status codes (e.g., 200 for creation instead of 201)
- Forgetting
Live Coding Suggestions:
- Start with just GET all users
- Add console.log statements to show req.params, req.body
- Intentionally break things (remove express.json(), forget return) to show errors
- Show what happens when you restart the server (data resets)
REST Conventions:
- Collection endpoints:
/api/users(no ID) - Single resource:
/api/users/:id(with ID) - Use plural nouns:
/usersnot/user - Avoid verbs in URLs:
/api/usersnot/api/getUsers
- Collection endpoints:
Interactive Exercise: “What HTTP method and endpoint would you use to: - Get user with ID 5? → GET /api/users/5 - Create a new user? → POST /api/users - Delete user with ID 3? → DELETE /api/users/3”
Testing the User Service - Complete Examples
Save the previous code as user-service.js, run it, then test:
Test 1: GET All Users
- open browser and visit:
http://localhost:3000/api/usersor
curl http://localhost:3000/api/usersResponse:
{
"success": true,
"count": 2,
"data": [
{ "id": 1, "name": "Alice", "email": "alice@example.com" },
{ "id": 2, "name": "Bob", "email": "bob@example.com" }
]
}Test 2: GET Single User
curl http://localhost:3000/api/users/1Response:
{
"success": true,
"data": { "id": 1, "name": "Alice", "email": "alice@example.com" }
}Test 3: POST Create New User
You cannot use browser for POST, so use curl or vscode REST client (https://marketplace.visualstudio.com/items?itemName=humao.rest-client).
curl -X POST http://localhost:3000/api/users \
-H "Content-Type: application/json" \
-d '{
"name": "Charlie",
"email": "charlie@example.com"
}'Response:
{
"success": true,
"message": "User created",
"data": { "id": 3, "name": "Charlie", "email": "charlie@example.com" }
}Test 4: DELETE User
curl -X DELETE http://localhost:3000/api/users/2Response:
{
"success": true,
"message": "User deleted"
}Verify deletion (GET all users again):
curl http://localhost:3000/api/users
# Charlie and Alice remain, Bob is gone!�🚀 fullstack-minimal-app Backend Tour
Now let’s explore our project’s starting point
# Navigate to backend
cd fullstack-minimal-app/backend
# Explore structure
ls -laWhat’s included:
backend/
├── src/
│ ├── app.js ← Express app setup
│ ├── routes/ ← API route handlers
│ │ └── products.js ← Product endpoints
│ ├── middleware/ ← Custom middleware
│ ├── config/ ← Database config
│ └── models/ ← Database models (later)
├── package.json ← Dependencies
└── server.js ← Server entry point
Start the backend:
npm install
npm run dev # Starts on http://localhost:4000Test it works:
curl http://localhost:4000/api/products
# Should return JSON array of products🏗️ Express App Structure
Understanding the Express foundation
server.js (Entry Point)
const app = require('./src/app');
const PORT = process.env.PORT || 4000;
app.listen(PORT, () => {
console.log(`✅ Server running on http://localhost:${PORT}`);
console.log(`📝 API docs: http://localhost:${PORT}/api`);
});app.js (Express Setup)
const express = require('express');
const cors = require('cors');
const morgan = require('morgan');
const app = express();
// Middleware
app.use(cors()); // Enable CORS for React frontend
app.use(morgan('dev')); // HTTP request logging
app.use(express.json()); // Parse JSON request bodies
app.use(express.urlencoded({ extended: true })); // Parse form data
// Routes
app.use('/api/products', require('./routes/products'));
// Health check endpoint
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString });
});
// Catch-all error handler
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: 'Something went wrong!' });
});
module.exports = app;Key Concepts:
- Middleware: Functions that process requests before reaching routes
- Routes: Define API endpoints and their handlers
- Error Handling: Centralized error processing
🛣️ REST API Design Principles
RESTful APIs follow predictable patterns
HTTP Methods & Status Codes
| Method | Purpose | Success Status | Example |
|---|---|---|---|
| GET | Retrieve data | 200 OK | Get all products |
| POST | Create new resource | 201 Created | Create product |
| PUT | Update entire resource | 200 OK | Update product |
| PATCH | Update partial resource | 200 OK | Update product name |
| DELETE | Remove resource | 200 OK or 204 No Content | Delete product |
URL Patterns
// Resource collections
GET /api/products // Get all products
POST /api/products // Create new product
// Specific resources
GET /api/products/:id // Get one product
PUT /api/products/:id // Update product
DELETE /api/products/:id // Delete product
// Nested resources
GET /api/products/:id/reviews // Get product reviews
POST /api/products/:id/reviews // Add review to productResponse Format Standards
// Success response
{
"success": true,
"data": { /* actual data */ },
"message": "Optional success message"
}
// Error response
{
"success": false,
"error": "Error message",
"details": { /* optional error details */ }
}
// Collection response
{
"success": true,
"data": [ /* array of items */ ],
"meta": {
"total": 150,
"page": 1,
"limit": 20
}
}📝 Building Product Routes
Let’s build the review aggregator API endpoints
routes/products.js
const express = require('express');
const router = express.Router();
const { products, reviews, getNextProductId, getNextReviewId } = require('../data/mockData');
const { fetchReviewsFromSources } = require('../services/mockScraper');
// GET /api/products - Get all products
router.get('/', (req, res) => {
try {
res.json({
success: true,
data: products,
count: products.length
});
} catch (error) {
res.status(500).json({
success: false,
error: 'Failed to fetch products'
});
}
});
// POST /api/products - Create new product
router.post('/', (req, res) => {
try {
const { name, description, price, image_url } = req.body;
// Basic validation
if (!name || !price) {
return res.status(400).json({
success: false,
error: 'Name and price are required'
});
}
const newProduct = {
id: getNextProductId(),
name,
description: description || '',
price: parseFloat(price),
image_url: image_url || '',
created_at: new Date()
};
products.push(newProduct);
res.status(201).json({
success: true,
data: newProduct
});
} catch (error) {
res.status(500).json({
success: false,
error: 'Failed to create product'
});
}
});
// POST /api/products/:id/fetch - Fetch reviews from external sources for a product
router.post('/:id/fetch', async (req, res, next) => {
try {
const productId = parseInt(req.params.id);
const product = products.find(p => p.id === productId);
if (!product) {
return res.status(404).json({ success: false, error: 'Product not found' });
}
const scrapedReviews = await fetchReviewsFromSources(productId, req.body || {});
let added = 0;
let skipped = 0;
for (const s of scrapedReviews) {
const exists = reviews.find(r =>
r.product_id === productId &&
r.source === s.source &&
r.external_id === s.external_id
);
if (exists) {
skipped++;
continue;
}
reviews.push({
id: getNextReviewId(),
product_id: productId,
...s,
fetched_at: new Date()
});
added++;
}
res.json({
success: true,
message: `Fetched ${scrapedReviews.length} reviews`,
data: { added, skipped, total: scrapedReviews.length }
});
} catch (err) {
next(err);
}
});
// GET /api/products/:id - Get single product
router.get('/:id', (req, res) => {
try {
const productId = parseInt(req.params.id);
const product = products.find(p => p.id === productId);
if (!product) {
return res.status(404).json({
success: false,
error: 'Product not found'
});
}
res.json({
success: true,
data: product
});
} catch (error) {
res.status(500).json({
success: false,
error: 'Failed to fetch product'
});
}
});
// GET /api/products/:id/reviews - List reviews for a product
router.get('/:id/reviews', (req, res) => {
const productId = parseInt(req.params.id);
const productReviews = reviews.filter(r => r.product_id === productId);
res.json({ success: true, data: productReviews, count: productReviews.length });
});
// GET /api/products/:id/aggregate - Aggregated stats for a product
router.get('/:id/aggregate', (req, res) => {
try {
const productId = parseInt(req.params.id);
const productReviews = reviews.filter(r => r.product_id === productId);
if (productReviews.length === 0) {
return res.json({
success: true,
data: {
overall: { average_rating: 0, total_reviews: 0, min_rating: 0, max_rating: 0 },
by_source: [],
rating_histogram: { "5": 0, "4": 0, "3": 0, "2": 0, "1": 0 }
}
});
}
const totalRating = productReviews.reduce((sum, r) => sum + r.rating, 0);
const avgRating = totalRating / productReviews.length;
const minRating = Math.min(...productReviews.map(r => r.rating));
const maxRating = Math.max(...productReviews.map(r => r.rating));
const bySource = {};
productReviews.forEach(r => {
bySource[r.source] ??= { total: 0, count: 0 };
bySource[r.source].total += r.rating;
bySource[r.source].count += 1;
});
const sourceBreakdown = Object.keys(bySource).map(source => ({
source,
average_rating: Math.round((bySource[source].total / bySource[source].count) * 10) / 10,
review_count: bySource[source].count
}));
const histogram = { "5": 0, "4": 0, "3": 0, "2": 0, "1": 0 };
productReviews.forEach(r => {
const bucket = Math.round(r.rating).toString();
if (histogram[bucket] !== undefined) histogram[bucket]++;
});
res.json({
success: true,
data: {
overall: {
average_rating: Math.round(avgRating * 10) / 10,
total_reviews: productReviews.length,
min_rating: minRating,
max_rating: maxRating
},
by_source: sourceBreakdown,
rating_histogram: histogram
}
});
} catch {
res.status(500).json({ success: false, error: 'Failed to calculate aggregate statistics' });
}
});
module.exports = router;🤖 AI Practice: Review Endpoints
Prompt for AI:
Context: Express.js REST API for product review aggregator
Task: Add review endpoints to existing products router
Endpoints needed:
1. POST /api/products/:id/fetch - Simulate fetching reviews from external scraper
2. GET /api/products/:id/reviews - Get all reviews for a product
3. GET /api/products/:id/aggregate - Get review statistics
Mock data structure for reviews:
{
id: number,
productId: number,
source: "Amazon" | "BestBuy" | "Walmart",
author: string,
rating: number (1-5),
title: string,
content: string,
createdAt: date
}
Requirements:
- Proper error handling and status codes
- JSON response format with success/error pattern
- Input validation for POST requests
- Mock external API delay (1-2 seconds)
- Calculate aggregate stats (average rating, count by source)
Output: Complete Express router code to add to products.js
After AI generates: Test endpoints with curl or Postman
🧪 Testing API Endpoints
Let’s test our API with different tools
Using curl
# Get all products
curl http://localhost:4000/api/products
# Get specific product
curl http://localhost:4000/api/products/1
# Create new product
curl -X POST http://localhost:4000/api/products \
-H "Content-Type: application/json" \
-d '{
"name": "Gaming Keyboard",
"description": "RGB mechanical keyboard",
"price": 89.99,
"category": "Electronics"
}'
# Fetch reviews for product
curl -X POST http://localhost:4000/api/products/1/fetch
# Get reviews
curl http://localhost:4000/api/products/1/reviews
# Get review stats
curl http://localhost:4000/api/products/1/aggregateUsing VS Code REST Client
Create test.http file:
### Get all products
GET http://localhost:4000/api/products
### Get specific product
GET http://localhost:4000/api/products/1
### Create new product
POST http://localhost:4000/api/products
Content-Type: application/json
{
"name": "Smart Watch",
"description": "Fitness tracking smartwatch",
"price": 299.99,
"category": "Electronics"
}
### Fetch reviews
POST http://localhost:4000/api/products/1/fetch
You should now have: You should now have:
- ✅ Express server running on port 4000
- ✅ Product CRUD endpoints working
- ✅ Review endpoints (fetch, get, aggregate)
- ✅ Proper error handling and JSON responses
- ✅ API testing experience with curl/REST client