Node.js & Express Backend Development

Building REST APIs for Full-Stack Applications

nodejs
express
backend
API development
Lecture## Node.js & Express Backend 🚀 {.center}
Auteur
Affiliations

Université de Toulon

LIS UMR CNRS 7020

Date de publication

2025-10-21

Résumé

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 express

Step 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:3000

In 3 steps, you have a working web server! 🎉

Teaching Points:

  1. Step 1 - Project Setup: Emphasize that npm init -y creates package.json (project manifest). The -y flag accepts all defaults. This is where dependencies are tracked.

  2. Step 2 - Understanding the Code:

    • require('express'): Imports the Express library
    • express(): Creates an Express application instance
    • app.use(express.json()): Middleware that parses incoming JSON - CRITICAL for POST requests
    • app.get('/'): Defines a route handler for GET requests to root path
    • (req, res): Request and response objects - the heart of Express
    • res.json(): Sends JSON response with proper Content-Type header
    • app.listen(): Starts the HTTP server on specified port
  3. Common Student Mistakes:

    • Forgetting npm install before running
    • Not using express.json() then wondering why POST data is undefined
    • Port already in use errors - teach lsof -ti:3000 | xargs kill
  4. 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:

  1. 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
  2. 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
  3. 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 requests
    • req.headers: HTTP headers
    • res.json(): Send JSON response
    • res.status(): Set HTTP status code
    • res.send(): Send plain text/HTML
  4. 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() and res.send()?” → json() sets Content-Type to application/json and stringifies the object
  5. 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:

  1. 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
  2. Code Walkthrough - Key Patterns:

    Pattern 1: Consistent Response Format

    // Always include success flag and structured data
    { success: true, data: {...} }  // Success
    { success: false, error: "..." } // Error

    This makes client-side handling predictable!

    Pattern 2: URL Parameters

    app.get('/api/users/:id', ...)  // Define with :id
    req.params.id                    // Access in handler

    The :id is a variable placeholder. /api/users/42req.params.id = "42"

    Pattern 3: Request Body

    app.use(express.json())  // MUST have this middleware!
    req.body.name            // Access POST data

    Without express.json(), req.body is 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)
  3. 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)
  4. Common Student Mistakes:

    • Forgetting parseInt() on req.params.id → string comparison fails
    • Not using return with 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)
  5. 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)
  6. REST Conventions:

    • Collection endpoints: /api/users (no ID)
    • Single resource: /api/users/:id (with ID)
    • Use plural nouns: /users not /user
    • Avoid verbs in URLs: /api/users not /api/getUsers

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/users or
curl http://localhost:3000/api/users

Response:

{
  "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/1

Response:

{
  "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/2

Response:

{
  "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 -la

What’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:4000

Test 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 product

Response 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

Generate Review API 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/aggregate

Using 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

✅ Hour 1 Checkpoint

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

Réutilisation