CSCI 4513
Week 14, Lecture 24
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.
// 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)');
});
Lazy in a good way! Programmers batched recycled code together and called it a framework.
A framework is built with a programming language
Much easier than raw Node.js! Express handles routing, middleware, and request/response for us.
// 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
// 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
// β 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
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!"
// β
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!)
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
// 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");
});
// 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
// β 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");
});
// 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: ?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();
});
URL: https://www.youtube.com/watch?v=xm3YgoEiEDc&t=424s
Example: All book routes in bookRouter.js, all author routes in authorRouter.js
// 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;
// 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")
MVC Pattern: Model-View-Controller
Controllers are just middleware functions with specific responsibilities!
// 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 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.
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);
});
// 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'));
// 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");
});
// 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 };
// 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 };
// routes/authorRouter.js
const { Router } = require("express");
const { getAuthorById } = require('../controllers/authorController');
const authorRouter = Router();
authorRouter.get("/:authorId", getAuthorById);
module.exports = authorRouter;
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");
}
}
// 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.
// errors/CustomNotFoundError.js
class CustomNotFoundError extends Error {
constructor(message) {
super(message);
this.statusCode = 404;
this.name = "NotFoundError";
}
}
module.exports = CustomNotFoundError;
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}`);
}
// 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();
});
// 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");
<!-- <% %> - 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>
<% }) %>
<!-- 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>
// 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" }
]
});
});
// 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!
<!-- 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>
// 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">
Build a simple message board app!
// 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
// 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()
}
];
// routes/indexRouter.js
router.get("/", (req, res) => {
res.render("index", {
title: "Mini Messageboard",
messages: 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>
<!-- 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>
// 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!
// 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("/");
});
// 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 });
});
Watch this 10-minute overview of the MVC pattern
// 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" }
});
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
}
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
π Next Class: Express - Part 2
Topics Coming:
π Homework: Complete Mini Message Board project