5. Middleware, Serving Static Assets, and Deployment
Follow along with code examples here!
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:
Essential Questions
By the end of this lesson, you should be able to answer these questions:
What is middleware? How does it differ from a regular controller?
What is the role of
next()in middleware? What happens if middleware never callsnext()and never sends a response?What are static assets? What is a static web server?
How do you serve static assets in an Express server?
What is a same-origin request? Why can you use a relative path like
/api/datawhen the frontend is served by the same Express server?
Key Concepts
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.
next()— a function passed to every middleware and controller that, when called, passes the request to the next middleware or controller in the chain. Middleware must callnext()(or send a response) to avoid leaving the request hanging.pathmodule - a built-in Node module for constructing absolute file paths in a cross-platform way.__dirname— a Node variable that holds the absolute path of the directory containing 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.
Vite projects can be "built" to bundle and minify source files into an optimized
distfolder — this is not strictly required for Vanilla JS (browsers support ES modules natively) but is recommended for performance. It is required for React projects.
Static Web Server - a server that stores static assets and serves them directly to visiting clients, without generating content programmatically.
express.static()- a built-in Express function that generates middleware for serving files from a specified directory. When a request matches a file in that directory, the file is sent as the response; otherwise, the request is passed to the next middleware or controller.Same-Origin Fetch - when a frontend is served by the same host as the API, fetch requests can use relative paths (e.g.,
/api/data) instead of absolute URLs. The browser sends the request to the same host that served the page.Continuous Deployment - a practice where every new code commit automatically triggers a redeploy of the application. On Render, this means the build and start commands run on every commit, keeping the live server in sync with the repository.
Express Review
Remember how the Express app works?
A client sends a request to a particular endpoint provided by the server.
The server receives the request and routes it to the proper controller based on the specific endpoint.
The controller looks at the request and generates a response which is sent to the client.
The client receives the response.
A controller is a callback function that parses a request and sends a response. Controllers are connected to a particular endpoint using app.get(endpoint, controller). The app will invoke the given controller when a request for the given endpoint is received. Every controller is invoked with three values:
A
reqobject which holds data related to the request, including query parameters.A
resobject which has methods to send a response.A
nextfunction, typically only used by "Middleware".
And here is how we can create a server with two endpoints (/api/users and /api/hello) and a fallback response.
In this example, we use app.use() to add a "fallback" controller to our app. Notice that we don't specify an endpoint or a method type: this means that the serve404 controller can run for any request type and any URL. However, if any of the previous controllers send a response, then serve404 won't run. Because of this, we must place this endpoint last (after the other calls to app.get()).
Q: What would happen if we put app.use(serve404) before the other calls to app.get()?
The serve404 controller would be invoked for any request method and URL preventing the other controllers from ever running.
In this lesson, we'll see how we can intentionally use app.use() BEFORE all of our endpoints to do something to each request before passing them along to the appropriate controller to send a response.
Now its time to learn about that next method!
Middleware and next()
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/helloor/api/data, etc...)the request method (
GETorPOST, 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:
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.useto register the middleware which means the function is invoked for ALL endpointsWe use
next()instead ofres.send()(or other response methods).next()invokes the next middleware / controller registered to handle the current request.
We first create the
logRoutesfunction to print out information about the requestAt the end, we invoke
next(), passing along the request to one of our controllers to send a response.We register
logRoutesusingapp.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!)Rate limiter middleware like
express-rate-limitError handling middleware like
errorhandler
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
When you visit a website, like https://google.com, you are immediately presented with a rendered website. What's happening there?

Now, imagine that the website is just the "static assets" of a Vite 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 frontend 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 Vite Static Assets
Check out the frontend/ directory in the repo for this lesson. So far, we've used Vite's development server to get these frontend static assets at http://localhost:5173.
Now, we can have our own server to provide access to those static assets! Back in the server, we could add the following endpoint and controller:
Why an absolute path?
__dirnameis a Node variable that holds the absolute path of the directory containing the current file. A bare relative path like'../frontend'is resolved from the process's working directory, which can vary depending on which directory you start the server from. Using__dirnamewithpath.join()produces a path that works no matter where the server is started from.
This code serves the index.html file, but that file also needs access to /src/main.js and /src/style.css. Open the Console in your browser and you can see that those files are not being found.
So, we need two more controllers:
Now, imagine that your application has hundreds of static assets! You would need an endpoint and controller for every file you'd want to serve.
express.static() Middleware
express.static() MiddlewareRather 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 file path to the folder containing our static assets:
Explanation:
Now, we just make a file path to the entire
frontendfolder and pass the file path toexpress.static()which returns a middleware function which we callserveStaticapp.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 clientOrder 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.
Fetch Requests to the Same Origin
When the frontend is served by the same Express server that handles your API, fetch requests can use relative paths.
In the main.js file of the demo frontend, notice how fetch requests are made:
When a browser makes a fetch request with a relative path like /api/data, it sends the request to the same host that served the page.
If the page was served from...
/api/data resolves to...
http://localhost:8080
http://localhost:8080/api/data
https://your-app.onrender.com
https://your-app.onrender.com/api/data
This means the same code works in development and in production without any changes.
The problem with running the Vite dev server separately
When developing, you might be tempted to use Vite's dev server (npm run dev), which typically runs on http://localhost:5173. But if the page is served from port 5173 and the Express server is on port 8080, a relative path like /api/data resolves to http://localhost:5173/api/data — pointing at Vite, not Express.
To make it work, you'd have to write an absolute URL:
The solution: serve the frontend from Express
By using express.static() to serve the frontend from the same Express server, the frontend and API share the same origin. Relative paths in fetch requests resolve correctly in both development and production — no hardcoded URLs needed.
Deploying Web Server to Render
Github Pages provides static site hosting.
This means that the server that Github Pages runs on your behalf can only send static files to the client (HTML, CSS, and JS files).
Github Pages static sites are not capable of receiving or sending messages via HTTP.
Render provides web service and database hosting (it can also host static sites).
This means that the server that Render runs on your behalf can send static assets, receive and send messages via HTTP, and interact with a database.
Render also can host your database giving you a one-stop-shop for running your fullstack application.
Start by creating an account using your GitHub account. This will let you easily deploy straight from a GitHub repository. This will take you to your Dashboard where you can see existing deployments.

Create a New Web Service
Make sure you are signed in using your GitHub account
https://dashboard.render.com/ and click on New +
Select Web Service
Choose Git Provider to find a repository on your account. It may take some time for your repositories to load.
If your repository is public, you can provide a link to the repository but it will not be able to auto-deploy on future commits. As such, this is NOT the preferred method.
Fill out the information for your Server
Name - the name of your app (it will appear in the URL that render gives you. For example:
app-name-here.onrender.com)Language - Node
Branch -
draftfor assignments,mainfor portfolio projectsRegion - select US East (Ohio)
Root Directory - Leave blank (will default to the root of your repo)
Build Command:
If your application has a database, see the next section
If you need to build your frontend
If neither:
Start Command (assuming your
index.jsfile is inserver/):Instance Type - select Free
Add Any environment variables your application may need:

Add environment variables individually or paste multiple values at a time from a .env file. Select Deploy Web Service
This should take you to your web service's dashboard where you can see the latest build information and the URL. In a few minutes your server will be up and running!
Any time that you want to make an update to your deployed server, just commit and push the change to your repo! The deployment process will automatically run your "Build" and "Start" commands (unless you used a public git URL to setup your server in which cause auto-deployments are disabled).

Best Practice — Serving the Dist Folder and Continuous Deployment
For Vite projects, running npm run build bundles and minifies the static asset files into an optimized dist folder. All JavaScript and CSS is condensed into one file each. When we deploy our project, this means fewer requests, smaller files, and hashed filenames for cache-busting for our users.
In the provided repo, build the dist folder and note the file structure:
To have our deployed server use this dist/ version of the frontend, update the pathToFrontend variable to reference the dist/ directory — but only when running in a "production environment" (when they are deployed).
Render (and most hosting services) automatically sets the environment variable
process.env.NODE_ENVto'production', which we use to conditionally set the file path.
In our deployment configuration, the "Build" and "Start" commands are executed before every new deploy as a part of the "Auto Deploy" process. Render will redeploy your application on every new commit. This means the dist/ assets are always rebuilt before the server restarts, so any frontend changes are reflected in the new deployment.
After the "Build" command runs, the "Start" command runs to start the server. To ensure that our server works properly, we just need to make sure the server dependencies are installed and then run the index.js file with node:

As a result, the continuous deployment process would look like this:
A commit is made with changes to the project
Render detects the commit and begins a new deployment
The "build" command is executed, generating updated static assets
The "start" command is executed, starting the server
The deployment completes and the server is live!
Complete Code
Last updated