Express Part 1

Express.js - Part 1

Web Framework Fundamentals

CSCI 4513

Week 14, Lecture 24

Today's Learning Objectives

Norse Mythology Connection

Heimdall & GjallarbrΓΊ - The Guardian and the Bridge

Heimdall guards GjallarbrΓΊ, the bridge to Asgard, deciding who may pass and directing travelers to their destinationsβ€”just as Express routes and middleware control the flow of requests through your application.

The Tale:

Heimdall, the watchman of the gods, stands eternal guard at GjallarbrΓΊ, the golden bridge that leads to Asgard. With eyesight so keen he can see for hundreds of miles and hearing so sharp he can detect grass growing, Heimdall examines every traveler who approaches the bridge. He doesn't simply allow or deny passageβ€”he directs each visitor to their proper destination within the realm, ensuring they reach the right hall, the right god, or the right purpose for their journey.

When a request arrives at the bridge, Heimdall first validates it (middleware authentication). Then he examines its purpose and destination (routing). If the traveler seeks Thor, Heimdall directs them along the path to Bilskirnir. If they seek Odin, he routes them to Valaskjalf. Each traveler passes through Heimdall's inspection (middleware chain) before reaching their final destination (controller). This organized system of inspection and routing prevents chaos and ensures every request reaches exactly where it needs to go.

The Framework as Guardian

// Heimdall examining travelers at GjallarbrΓΊ
const express = require('express');
const app = express(); // The bridge to Asgard

// Heimdall's inspection (middleware)
app.use((req, res, next) => {
    console.log(`Traveler approaching: ${req.method} ${req.path}`);
    next(); // Allow passage to next checkpoint
});

// Routes to different halls in Asgard
app.get('/thor', (req, res) => {
    res.send('Welcome to Bilskirnir - Thor\'s Hall');
});

app.get('/odin', (req, res) => {
    res.send('Welcome to Valaskjalf - Odin\'s Hall');
});

// Unknown destinations
app.use((req, res) => {
    res.status(404).send('This hall does not exist in Asgard');
});

app.listen(3000, () => {
    console.log('Heimdall stands watch at GjallarbrΓΊ (port 3000)');
});

Reflection Questions

What is a Framework?

Lazy in a good way! Programmers batched recycled code together and called it a framework.

Programming Language vs Framework

Language

  • JavaScript
  • Python
  • Ruby
  • Java

Frameworks

  • Express, React (JS)
  • Django, Flask (Python)
  • Rails, Sinatra (Ruby)
  • Spring (Java)

A framework is built with a programming language

What Problems Do Frameworks Solve?

Popular Front-end Frameworks

Popular Back-end Frameworks

What is Express?

Much easier than raw Node.js! Express handles routing, middleware, and request/response for us.

Setting Up Express

// 1. Create a new project directory
mkdir my-express-app
cd my-express-app

// 2. Initialize npm
npm init -y

// 3. Install Express
npm install express

// 4. Create app.js

Your First Express Server

// app.js
const express = require("express");
const app = express();

app.get("/", (req, res) => res.send("Hello, world!"));

const PORT = 3000;
app.listen(PORT, (error) => {
  // This is important!
  // Without this, startup errors will silently fail
  if (error) {
    throw error;
  }
  console.log(`Express app listening on port ${PORT}!`);
});

Run: node app.js

Visit: http://localhost:3000

Port Variable Best Practice

// ❌ Hardcoded port
const PORT = 3000;

// βœ… Use environment variable with fallback
const PORT = process.env.PORT || 3000;

// Why?
// - Hosting services often set their own PORT
// - Easy to change without editing code
// - Can be different for dev/staging/production

A Request's Journey

1. Browser sends GET request to http://localhost:3000/
   ↓
2. Express receives request, creates req object
   ↓
3. Express passes request through middleware chain
   ↓
4. Request matches route: app.get("/", ...)
   ↓
5. Route handler executes: (req, res) => res.send("Hello, world!")
   ↓
6. Response sent back to browser
   ↓
7. Browser displays "Hello, world!"

Auto-Restarting Your Server

// βœ… RECOMMENDED: Use Node's built-in watch mode
node --watch app.js

// Alternative: Use Nodemon (install first)
npm install -g nodemon
nodemon app.js

// Both will automatically restart your server
// when you make changes to your code!

πŸ’‘ Node's --watch flag is the simplest method (no extra packages needed!)

The Anatomy of a Route

app.get("/", (req, res) => res.send("Hello, world!"));
// β”‚   β”‚     β”‚    └─ Response object
// β”‚   β”‚     └────── Request object
// β”‚   └──────────── Path to match
// └──────────────── HTTP verb (GET, POST, etc.)

This route matches GET requests to the / path

HTTP Verbs

Route Examples

// GET request to /
app.get("/", (req, res) => {
  res.send("Homepage");
});

// POST request to /messages
app.post("/messages", (req, res) => {
  res.send("Creating a new message");
});

// Match all verbs to /api
app.all("/api", (req, res) => {
  res.send("API endpoint");
});

Paths

// String paths
"/messages"           // Matches exactly /messages
"/messages/all"       // Matches exactly /messages/all

// Optional characters with {}
"/message{s}"         // Matches /message and /messages
"/{messages}"         // Matches / and /messages
"/foo{/bar}/baz"      // Matches /foo/baz and /foo/bar/baz

// Wildcards with * (must be followed by a name)
"/{*splat}"           // Matches /, /anything, /foo/bar/baz

Order Matters!

// ❌ BAD - Second route never reached
app.get("/{*splat}", (req, res) => {
  res.send("Catch-all route");
});
app.get("/messages", (req, res) => {
  res.send("This will never run!");
});

// βœ… GOOD - Specific routes first
app.get("/messages", (req, res) => {
  res.send("Messages page");
});
app.get("/{*splat}", (req, res) => {
  res.send("404 - Page not found");
});

Route Parameters

// Define route with :username parameter
app.get("/:username/messages", (req, res) => {
  console.log(req.params);
  // GET /odin/messages β†’ { username: "odin" }
  // GET /thor/messages β†’ { username: "thor" }

  res.send(`Messages for ${req.params.username}`);
});

// Multiple parameters
app.get("/:username/messages/:messageId", (req, res) => {
  console.log(req.params);
  // GET /odin/messages/123 β†’ { username: "odin", messageId: "123" }

  res.send(`Message ${req.params.messageId} from ${req.params.username}`);
});

Query Parameters

// Query parameters: ?key=value&key2=value2
app.get("/:username/messages", (req, res) => {
  console.log("Params:", req.params);
  console.log("Query:", req.query);

  // GET /odin/messages?sort=date&direction=ascending
  // Params: { username: "odin" }
  // Query: { sort: "date", direction: "ascending" }

  // Repeated keys become arrays
  // GET /odin/messages?sort=date&sort=likes
  // Query: { sort: ["date", "likes"] }

  res.end();
});

Real-World Example: YouTube

URL: https://www.youtube.com/watch?v=xm3YgoEiEDc&t=424s

  • Path: /watch
  • Query Parameters:
    • v = xm3YgoEiEDc (video ID)
    • t = 424s (start time)

Routers - Organizing Routes

Example: All book routes in bookRouter.js, all author routes in authorRouter.js

Creating a Router

// routes/authorRouter.js
const { Router } = require("express");

const authorRouter = Router();

authorRouter.get("/", (req, res) => {
  res.send("All authors");
});

authorRouter.get("/:authorId", (req, res) => {
  const { authorId } = req.params;
  res.send(`Author ID: ${authorId}`);
});

module.exports = authorRouter;

Using Routers in App

// app.js
const express = require("express");
const app = express();
const authorRouter = require("./routes/authorRouter");
const bookRouter = require("./routes/bookRouter");
const indexRouter = require("./routes/indexRouter");

// Mount routers on paths
app.use("/authors", authorRouter);
app.use("/books", bookRouter);
app.use("/", indexRouter);

// Now /authors routes work!
// GET /authors β†’ authorRouter.get("/")
// GET /authors/123 β†’ authorRouter.get("/:authorId")

What is a Controller?

MVC Pattern: Model-View-Controller

Controllers are just middleware functions with specific responsibilities!

Response Methods

// res.send - General purpose, auto-sets Content-Type
res.send("Hello");
res.send({ name: "Alice" });  // Automatically converts to JSON

// res.json - Explicit JSON response
res.json({ user: "Alice", age: 30 });

// res.redirect - Redirect to different URL
res.redirect("/login");

// res.render - Render a view template (we'll cover this soon!)
res.render("index", { title: "Home" });

// res.status - Set status code (can chain with others)
res.status(404).send("Not found");
res.status(201).json({ message: "Created" });

res.send vs res.json

// res.send automatically handles JSON for objects
res.send({ name: "Alice" });  // Works, sends JSON

// res.json always converts to JSON (even non-objects)
res.json(123);  // "123" as JSON
res.json(true); // "true" as JSON

// Best practice: Use res.json for JSON responses
// It's explicit and clear about intent!

⚠️ Response methods don't exit the function! Use return if needed.

What is Middleware?

Middleware Example

function myMiddleware(req, res, next) {
  // Log the request
  console.log("Middleware function called");

  // Modify the request object
  req.customProperty = "Hello from myMiddleware";

  // Pass control to next middleware/route handler
  next();
}

app.use(myMiddleware);

app.get("/", (req, res) => {
  // Can access req.customProperty here!
  res.send(req.customProperty);
});

Application-Level Middleware

// Runs for every request
app.use((req, res, next) => {
  console.log(`${req.method} ${req.path}`);
  next();
});

// Built-in middleware - parse JSON bodies
app.use(express.json());

// Built-in middleware - parse URL-encoded bodies (forms)
app.use(express.urlencoded({ extended: true }));

// Built-in middleware - serve static files
app.use(express.static('public'));

Router-Level Middleware

// Only runs for routes in this router
const authorRouter = Router();

// This middleware only runs for /authors routes
authorRouter.use((req, res, next) => {
  console.log("Author route accessed");
  next();
});

authorRouter.get("/", (req, res) => {
  res.send("All authors");
});

Creating a Controller

// db.js (mock database)
const authors = [
  { id: 1, name: "Bryan" },
  { id: 2, name: "Christian" },
  { id: 3, name: "Jason" },
];

async function getAuthorById(authorId) {
  return authors.find(author => author.id === authorId);
}

module.exports = { getAuthorById };

Controller Example

// controllers/authorController.js
const db = require("../db");

async function getAuthorById(req, res) {
  const { authorId } = req.params;

  const author = await db.getAuthorById(Number(authorId));

  if (!author) {
    res.status(404).send("Author not found");
    return; // Important! Stops function execution
  }

  res.send(`Author Name: ${author.name}`);
}

module.exports = { getAuthorById };

Using Controllers in Routes

// routes/authorRouter.js
const { Router } = require("express");
const { getAuthorById } = require('../controllers/authorController');

const authorRouter = Router();

authorRouter.get("/:authorId", getAuthorById);

module.exports = authorRouter;

Handling Errors with try/catch

async function getAuthorById(req, res) {
  const { authorId } = req.params;

  try {
    const author = await db.getAuthorById(Number(authorId));

    if (!author) {
      res.status(404).send("Author not found");
      return;
    }

    res.send(`Author Name: ${author.name}`);
  } catch (error) {
    console.error("Error retrieving author:", error);
    res.status(500).send("Internal Server Error");
  }
}

Error Handler Middleware

// Must have FOUR parameters to be recognized as error handler
app.use((err, req, res, next) => {
  console.error(err);
  res.status(err.statusCode || 500).send(err.message);
});

// Place at the END of your app.js file!
// This catches all errors from previous middleware

⚠️ Must have 4 parameters! Even if you don't use all of them.

Custom Error Classes

// errors/CustomNotFoundError.js
class CustomNotFoundError extends Error {
  constructor(message) {
    super(message);
    this.statusCode = 404;
    this.name = "NotFoundError";
  }
}

module.exports = CustomNotFoundError;

Using Custom Errors

const CustomNotFoundError = require("../errors/CustomNotFoundError");

async function getAuthorById(req, res) {
  const { authorId } = req.params;

  const author = await db.getAuthorById(Number(authorId));

  if (!author) {
    // Express auto-catches thrown errors in async functions
    throw new CustomNotFoundError("Author not found");
  }

  res.send(`Author Name: ${author.name}`);
}

The next() Function

// next() - Pass control to next middleware
app.use((req, res, next) => {
  console.log("Middleware 1");
  next();
});

// next(error) - Pass control to error handler
app.use((req, res, next) => {
  next(new Error("Something went wrong!"));
});

// next('route') - Skip to next route handler
app.get("/user", (req, res, next) => {
  if (!req.user) return next('route');
  res.send("User found");
});

// next('router') - Exit current router
router.use((req, res, next) => {
  if (!authorized) return next('router');
  next();
});

Views with EJS

Setting Up EJS

// 1. Install EJS
npm install ejs

// 2. Create views folder
mkdir views

// 3. Configure Express (app.js)
const path = require("node:path");

app.set("views", path.join(__dirname, "views"));
app.set("view engine", "ejs");

EJS Syntax

<!-- <% %> - JavaScript code (no output) -->
<% const name = "Alice" %>

<!-- <%= %> - Output variable (escaped) -->
<p>Hello, <%= name %>!</p>

<!-- <%- %> - Output variable (unescaped HTML) -->
<div><%- htmlContent %></div>

<!-- Loops -->
<% animals.forEach((animal) => { %>
  <li><%= animal %></li>
<% }) %>

Creating an EJS Template

<!-- views/index.ejs -->
<!DOCTYPE html>
<html>
<head>
  <title><%= title %></title>
</head>
<body>
  <h1><%= message %></h1>
  <ul>
    <% users.forEach((user) => { %>
      <li><%= user.name %></li>
    <% }) %>
  </ul>
</body>
</html>

Rendering a View

// app.js or in a controller
app.get("/", (req, res) => {
  res.render("index", {
    title: "Home Page",
    message: "Welcome!",
    users: [
      { name: "Alice" },
      { name: "Bob" },
      { name: "Charlie" }
    ]
  });
});

The locals Variable

// In your view, you can access variables via locals
<%= locals.message %>  // Safe, returns undefined if not defined
<%= message %>          // Throws error if not defined

// res.locals is available across middleware
app.use((req, res, next) => {
  res.locals.currentUser = req.user;
  next();
});

// Now all views have access to currentUser!

Reusable Templates with include

<!-- views/navbar.ejs -->
<nav>
  <ul>
    <% links.forEach((link) => { %>
      <li><a href="<%= link.href %>"><%= link.text %></a></li>
    <% }) %>
  </ul>
</nav>

<!-- views/index.ejs -->
<body>
  <%- include('navbar', {links: links}) %>
  <!-- Rest of page -->
</body>

Serving Static Assets

// app.js
const assetsPath = path.join(__dirname, "public");
app.use(express.static(assetsPath));

// File structure:
// public/
// β”œβ”€β”€ styles.css
// β”œβ”€β”€ script.js
// └── images/
//     └── logo.png

// In your EJS template:
<link rel="stylesheet" href="/styles.css">
<script src="/script.js"></script>
<img src="/images/logo.png">

Project: Mini Message Board

Build a simple message board app!

  • Display messages on index page
  • Form to add new messages
  • View individual message details
  • Use Express, EJS, and middleware

Project Setup

// 1. Create project
mkdir mini-message-board
cd mini-message-board
npm init -y
npm install express ejs

// 2. Create folders
mkdir views routes controllers

// 3. Create app.js

Sample Messages Array

// At the top of your index router
const messages = [
  {
    text: "Hi there!",
    user: "Amando",
    added: new Date()
  },
  {
    text: "Hello World!",
    user: "Charles",
    added: new Date()
  }
];

Index Route - Display Messages

// routes/indexRouter.js
router.get("/", (req, res) => {
  res.render("index", {
    title: "Mini Messageboard",
    messages: messages
  });
});

Index View - Loop Through Messages

<!-- views/index.ejs -->
<h1><%= title %></h1>

<% messages.forEach((message) => { %>
  <div class="message">
    <p><strong><%= message.user %></strong></p>
    <p><%= message.text %></p>
    <p><small><%= message.added %></small></p>
  </div>
<% }) %>

<a href="/new">New Message</a>

New Message Form

<!-- views/form.ejs -->
<h1>New Message</h1>

<form method="POST" action="/new">
  <div>
    <label for="author">Author:</label>
    <input type="text" id="author" name="author" required>
  </div>

  <div>
    <label for="message">Message:</label>
    <textarea id="message" name="message" required></textarea>
  </div>

  <button type="submit">Send</button>
</form>

Parsing Form Data

// app.js - IMPORTANT!
// This middleware parses form data into req.body
app.use(express.urlencoded({ extended: true }));

⚠️ Without this middleware, req.body will be undefined!

Handling Form Submission

// routes/indexRouter.js
router.post("/new", (req, res) => {
  const { author, message } = req.body;

  messages.push({
    text: message,
    user: author,
    added: new Date()
  });

  // Redirect back to homepage
  res.redirect("/");
});

Message Details Page

// Add "open" button in index.ejs
<% messages.forEach((message, index) => { %>
  <div class="message">
    <p><strong><%= message.user %></strong></p>
    <p><%= message.text %></p>
    <a href="/message/<%= index %>">Open</a>
  </div>
<% }) %>

// Route to show message details
router.get("/message/:id", (req, res) => {
  const message = messages[req.params.id];
  res.render("message", { message: message });
});

MVC Pattern Tutorial

Watch this 10-minute overview of the MVC pattern

Review: Express Fundamentals

Review: Route Parameters & Query

// Route parameters - part of the path
app.get("/:username/posts/:postId", (req, res) => {
  // req.params = { username: "alice", postId: "123" }
});

// Query parameters - after the ?
app.get("/search", (req, res) => {
  // GET /search?q=express&sort=date
  // req.query = { q: "express", sort: "date" }
});

Review: Middleware Flow

app.use(middleware1);     // Runs first
app.use(middleware2);     // Runs second
app.get("/", handler);    // Runs if path matches

function middleware1(req, res, next) {
  console.log("1");
  next(); // Pass to next middleware
}

function middleware2(req, res, next) {
  console.log("2");
  next(); // Pass to route handler
}

function handler(req, res) {
  console.log("3");
  res.send("Done");
  // Request-response cycle ends
}

Express Project Structure

my-express-app/
β”œβ”€β”€ app.js                   # Main app file
β”œβ”€β”€ package.json
β”œβ”€β”€ public/                  # Static assets
β”‚   β”œβ”€β”€ styles.css
β”‚   └── script.js
β”œβ”€β”€ views/                   # EJS templates
β”‚   β”œβ”€β”€ index.ejs
β”‚   β”œβ”€β”€ form.ejs
β”‚   └── partials/
β”‚       β”œβ”€β”€ navbar.ejs
β”‚       └── footer.ejs
β”œβ”€β”€ routes/                  # Route files
β”‚   β”œβ”€β”€ indexRouter.js
β”‚   β”œβ”€β”€ userRouter.js
β”‚   └── messageRouter.js
β”œβ”€β”€ controllers/             # Controller logic
β”‚   └── userController.js
└── errors/                  # Custom error classes
    └── CustomNotFoundError.js

Today's Takeaways

You Can Now:

Next Steps

πŸ“… Next Class: Express - Part 2

Topics Coming:

  • Form validation
  • Database integration
  • Sessions and authentication
  • Deployment

πŸ“ Homework: Complete Mini Message Board project

  • Build the message board with Express and EJS
  • Add all routes (index, new, message details)
  • Style it with CSS
  • Push to GitHub