Marcy Lab School Docs
  • Welcome
  • Student Guidelines & Policies
    • Student Handbook
    • AI Policy
    • Academic Calendar
  • Environment Setup
    • Local Environment Setup - Mac
    • Local Environment Setup - Windows
    • GitHub Setup
    • Postgres Setup
  • How-Tos
    • How To Code at Marcy: Code Style Guide
    • How to Do Short Response and Coding Assignments
    • How to Debug
    • How to PEDAC
    • How to Create A GitHub Organization and Scrumboard
    • How to Create Projects with Vite
    • How to Deploy on GitHub Pages
    • How to Deploy on Render
    • How to Test your API with Postman
  • Mod 0 - Command Line Interfaces, Git, and GitHub
    • Overview
    • 1. Command Line Interfaces
    • 2. Git & GitHub
    • 3. Git Pulling & Merging
    • 4. Git Branching & PRs
  • Mod 1 - JavaScript Fundamentals
    • Overview
    • 1. Intro to Programming
    • 2. Errors
    • 3. Node & Node Modules
    • 4. Variables, Functions & String Methods
    • 5. Control Flow, typeof, and Math
    • 6. Loops
    • 7. Arrays
    • 8. Objects
    • 9. Higher Order Functions: Callbacks
    • 10. Higher Order Functions: Array Methods
    • 11. Regex
  • Mod 2 - HTML, CSS & the DOM
    • Overview
    • 1. HTML
    • 2. CSS
    • 3. Accessibility (a11y)
    • 4. The Document Object Model (DOM) API
    • 5. Events
    • 6. Forms
    • 7. The Box Model and Positioning
    • 8. Flexbox
    • 9. Grid & Media Queries
    • 10. ESModules
    • 11. Vite
    • 12. LocalStorage
  • Mod 3 - Async & APIs
    • Overview
    • 1. Promises
    • 2. Fetch
    • 3. Building a Fetching App
    • 4. Async & Await
    • 5. A Generic Fetch Handler
  • Mod 4 - Project Week!
    • Important How Tos and Guides
      • How to Create a GitHub Organization and Scrum Board
      • How To Start a Project with Vite
      • How To Deploy a Project with GitHub Pages
    • Project Week Overview
    • Agile Methodologies
    • Deliverables & Milestones
    • Technical Requirements Checklist
    • Free API List
    • Collaborative GitHub
  • Mod 5 - Object-Oriented Programming
    • Overview
    • 1. Intro to OOP, Encapsulation, Factory Functions, and Closure
    • 2. Classes
    • 3. Private & Static
    • 4. UML Diagrams & Has Many/Belongs To Relationships
    • 5. Challenge: Implementing Has Many/Belongs To
    • 6. Inheritance
    • 7. Polymorphism
    • 8. Review and Practice
    • MDN: Object Prototypes
  • Mod 6 - Data Structures & Algorithms
    • Overview
    • Important How Tos and Guides
      • How to Debug
      • How to PEDAC
    • 1. Nodes & Linked Lists
    • 2. Singly & Doubly Linked Lists
    • 3. Stacks & Queues
    • 4. Recursion
    • 5. Trees
  • Mod 7 - React
    • Overview
    • Important How Tos and Guides
      • How to Create Projects with Vite
      • How to Deploy on GitHub Pages
    • 1. Intro to React
    • 2. Events, State, and Forms
    • 3. Fetching with useEffect
    • 4. React Router
    • 5. Building a Flashcards App
    • 6. React Context
    • 7. Global Context Pattern
  • Mod 8 - Backend
    • Overview
    • Important How Tos and Guides
      • How to Deploy on Render
      • How to Test your API with Postman
      • Postgres Setup
    • 1. Intro to Express
    • 2. Building a Static Web Server with Middleware
    • 3. Securing API Keys and Environment Variables
    • 4. RESTful CRUD API
    • 5. Model-View-Controller Architecture
    • 6. SQL and Databases
    • 7. JOIN (Association) SQL Queries
    • 8. Knex
    • 9. Your First Fullstack App!
    • 10. Migrations & Seeds
    • 11. Schema Design & Normalization
    • 12. Hashing Passwords with Bcrypt
    • 13. React Express Auth Template Overview
  • Mod 9 - Civic Tech Hackathon
    • Overview
    • Rubric
  • Mod 10 - Capstone
    • Overview
Powered by GitBook
On this page
  • Terms
  • Express Review
  • Middleware and next()
  • Static Web Servers
  • Serving React Static Assets
  • express.static() Middleware
  • Summary
  1. Mod 8 - Backend

2. Building a Static Web Server with Middleware

Previous1. Intro to ExpressNext3. Securing API Keys and Environment Variables

Last updated 1 month ago

Follow along with code examples !

In the last lecture, we learned about the basics of Express: endpoints and controllers. Today, we'll learn about a new kind of controller that will expand our server's capabilities: middleware.

Table of Contents:

Terms

  • Middleware - a function in express that intercepts and processes incoming HTTP requests. It can perform server-side actions such as parsing the request, modifying the response, or executing additional logic before passing control to the next middleware in the chain."

  • path module - a module for creating absolute paths to static assets

  • __dirname — an environment variable that returns the path to the parent directory of the current file.

  • Static Assets - unchanging files delivered to the client exactly as they are stored on a server. These include HTML, CSS, JavaScript files, images, videos, fonts, and documents. For React projects, we need to "build" our project to generate static assets (convert .jsx files to .js files).

Express Review

Remember how the Express app works?

  1. A client sends a request to the server.

  2. The server receives the request and routes it to the proper controller based on the specific endpoint.

  3. The controller processes the request, interacts with any necessary data or services, and generates a response.

  4. The server sends the response back to the client.

  5. The client is now free to do what it likes with the response.

And here is how we can create a server with two endpoints: /api/hello and /api/data

const express = require('express');
const app = express();

// When the endpoint is requested, controllers will send a response
const serveHello = (req, res, next) => {
  const name = req.query.name || "stranger"
  res.send(`hello ${name}`);
}
const serveData = (req, res, next) => {
  const data = [{ name: 'Carmen' }, { name: 'Maya' }, { name: 'Reuben' }];
  res.send(data)
}

// Define the method, endpoint URL, and controller
app.get('/api/hello', serveHello);
app.get('/api/data', serveData);

// Listen for requests on port 8080
const port = 8080;
app.listen(port, () => console.log(`listening at http://localhost:${port}`)); 

A controller is a callback function that parses a request and sends a response. It will be invoked asynchronously when the associated endpoint is sent a request.

Every controller is invoked with three values:

  • A req object which holds data related to the request, including query parameters.

  • A res object which has methods to send a response.

  • A next function, typically only used by "Middleware".

Now its time to learn about that next method!

Middleware and next()

When a server receives an HTTP request, it can do more than just send back a response. Often, the server will perform a number of server-side actions before a response is sent.

For example, suppose that you wanted the server to keep track of every request that is sent to it by printing out some information like:

  • the endpoint that was requested (/api/hello or /api/data, etc...)

  • the request method (GET or POST, etc...)

  • the time the request was received

Q: Why would it be helpful to log information about every incoming request?

Logging incoming HTTP requests can be incredibly helpful for debugging purposes.

Say you have 3 endpoints and one of them has a bug that causes the server to crash when an unexpected request is sent to it. If we print out every request that comes in to the server, we can simply look at the most recent request in the logs and know where to start debugging.

We can add this functionality to our serveHello controller, before we send the response, we can just add a few lines of code:

const serveHello = (req, res, next) => {
  // print the current time and request information 
  const timeOfRequest = new Date().toLocaleString();
  console.log(`${req.method}: ${req.originalUrl} - ${timeOfRequest}`);
  
  // then send the response
  const name = req.query.name || "stranger"
  res.send(`hello ${name}`);
}

However, now we also need to add this code to serveData. If we had more controllers, this would quickly become very repetitive.

Instead, we can use a middleware. Middleware in Express is a controller that can be invoked for all incoming requests before the final controller sends a response.

In many ways, middleware is like a controller. It receives the req, res, and next values. There are two key differences:

  • We use app.use to register the middleware which means the function is invoked for ALL endpoints

  • We use next() instead of res.send() (or other response methods).

    • next() invokes the next middleware / controller registered to handle the current request.

// Middleware function for logging route requests
const logRoutes = (req, res, next) => {
  const time = new Date().toLocaleString();
  console.log(`${req.method}: ${req.originalUrl} - ${time}`);
  next(); // Passes the request to the next middleware/controller
};
// Register the logRoutes middleware globally to log all requests
app.use(logRoutes);

// Other endpoints and controllers
  • We first create the logRoutes function to print out information about the request

  • At the end, we invoke next(), passing along the request to one of our controllers to send a response.

  • We register logRoutes using app.use() which causes it to be invoked for ALL endpoints.

  • Order matters! Middleware should be defined before controllers to ensure that it is invoked before the response is sent to the client.

With this middleware, we do not need to add this logging logic to every controller. Instead, this middleware will automatically be invoked for every incoming request before the final controller sends a response.

Sometimes, middleware can invoke res.send() if we want to interrupt the response cycle and send a response before it reaches the intended controller. In this way, middleware behaves like a guard clause. Most of the time, it won't send a response, but it can if needed.

Examples of this include:

  • Static asset middleware like express.static() (which you'll learn about next!)

Q: So, if a user sends a request to http://localhost:8080/api/hello, which functions are invoked and in what order?

First the logRoutes middleware is invoked. The next() function is called which passes the request to the next controller, serveHello.

Q: What would happen if the logRoutes controller DID send a response to the client? What would happen if it didn't invoke next()?

If logRoutes did invoke res.send(), the serveHello controller would NOT be invoked as a response has already been sent.

If we simply didn't invoke next(), our server would "hang" — the response would never be completed and the client would likely receive a timeout error because the request took too long.

Middleware can be custom-made like this logRoutes. However, we can also utilize some of the out-of-the-box middleware controllers provided by Express.

Static Web Servers

Now, imagine that the website is just the "static assets" of a React project deployed on GitHub pages! But instead of using GitHub pages, Google has its own servers to store those files and serve them to visiting users.

We call these static web servers because they store static assets (HTML, CSS, and JS files) and then provide a server application that serves those assets when requested.

Let's look at how we can serve the static assets of a React project from our server.

HTML, CSS, and JavaScript files are considered "static" because their content remains unchanged when being transferred from server to client.

APIs on the other hand serve dynamic content that changes depending on parameters of the request.

Serving React Static Assets

As we've learned, a React project's static assets are built into a folder called dist. In the provided repo, build the dist folder and note the file structure:

dist/
  - index.html
  - assets/
      - index-ABC123.js
      - index-ABC123.css

When a user visits the server's homepage /, we want to send the index.html file.

Back in the server, we could add the following endpoint and controller:

// The path module is useful for constructing relative filepaths
const path = require('path');

const serveIndexHTML = (req, res, next) => {
  // `path.join()` constructs an absolute file from the arguments
  // `__dirname` provides the absolute path of the current module's parent directory.
  const filepath = path.join(__dirname, '../vite-project/dist/index.html');
  res.sendFile(filepath);
};

app.get('/', serveIndexHTML);

If the frontend that we wanted to serve only consisted of a single index.html file, then this code above would suffice!

But the index.html file needs access to /assets/index-ABC123.js and /assets/index-ABC123.css. So we need two more controllers:

const serveIndexHTML = (req, res, next) => {
  const filepath = path.join(__dirname, '../vite-project/dist/index.html');
  res.sendFile(filepath);
}

const serveJS = (req, res, next) => {
  const filepath = path.join(__dirname, '../vite-project/dist/assets/index-ABC123.js');
  res.sendFile(filepath)
};

const serveCSS = (req, res, next) => {
  const filepath = path.join(__dirname, '../vite-project/dist/assets/index-ABC123.css');
  res.sendFile(filepath)
};

// Keep in mind, your js and css file names will change each time the dist folder is re-built
app.get('/', serveIndexHTML);
app.get('/assets/index-ABC123.js', serveJS);
app.get('/assets/index-ABC123.css', serveCSS);

Seems repetitive, no? Now, imagine that your application has hundreds of static assets!

express.static() Middleware

Rather than defining endpoints for every single static asset that you wish to serve, we can use the express.static() middleware generator included with Express.

express.static() is not middleware itself. Instead, invoking this function will generate a middleware function that we can use. We just need to provide a filepath to the folder containing our static assets:

// The path module is useful for constructing relative filepaths
const path = require('path');

// the filepath is to the entire assets folder
const filepath = path.join(__dirname, '../vite-project/dist');

// generate middleware using the filepath
const serveStatic = express.static(filepath);

// Register the serveStatic middleware before the remaining controllers
app.use(serveStatic);

// other controllers 

Explanation:

  • Now, we just make a filepath to the entire dist folder and pass the filepath to express.static() which returns a middleware function which we call serveStatic

  • app.use(serveStatic) will checks all incoming requests to see if they match files in the provided folder. if they do, they will be sent to the client

  • Order matters! Remember to add this before the rest of your controllers.

Like logRoutes, this middleware intercepts incoming requests before they reach the controllers. Unlike logRoutes, the middleware generated by express.static() can send a response to the client if a matching file is found. If not, it will pass the request to the controllers.

Summary

  • Controllers: Callback functions that handle requests by parsing them and sending responses.

  • Middleware Functions: Functions similar to controllers but pass requests to the next middleware without sending a response. They can also be executed for all requests while controllers typically handle a single endpoint.

  • Static Assets: Unchanging files (e.g., HTML, CSS, JS) served by a web server. For React projects, we need to "build" the project to convert "dynamic" .jsx files to "static" .js files

  • Serving Static Assets:

    1. Construct an absolute file path to the static assets folder using path.join() and __dirname.

    2. Use express.static(filepath) middleware to make static assets publicly available.

    3. Register the middleware with app.use()

Rate limiter middleware like

Error handling middleware like

When you visit a website, like , you are immediately presented with a rendered website. What's happening there?

express-rate-limit
errorhandler
https://google.com
here
Terms
Express Review
Middleware and next()
Static Web Servers
Serving React Static Assets
express.static() Middleware
Summary
Middleware in Express is a controller that can be invoked for all incoming requests before the final controller sends a response.
Google's Homepage