Streamlining Your Web Development: Serving SPAs Efficiently with Express

Streamlining Your Web Development: Serving SPAs Efficiently with Express

Simple Express server code snippet demonstrating a basic GET endpoint for server health check.

For a considerable time, influenced by the scale and technology stacks of projects I’ve been involved in, I’ve approached the client and server sides of applications as fundamentally separate entities. The client, in this model, operates as an independent application, often residing on a distinct host with its own configuration, code repository, and CI/CD pipelines. Within a PaaS environment like Azure, this paradigm is often logical due to the ease of scaling instances up and down, which contrasts with the potentially higher maintenance overhead of IaaS, where container and operating system upkeep falls on the user.

Introducing a Personal Project: Link Management Made Easy

Recently, I embarked on a personal side project: developing a link management service, similar to bit.ly, to enhance my social media link strategy. My primary goals are to retroactively repair broken links and deliver a more consistent user experience. While numerous open-source solutions exist, I saw this as an opportunity to build my own, driven by both enjoyment and a desire to learn.

Cost-effectiveness is a key consideration for any side project deployment. Domain names and hosting fees can accumulate rapidly when your work isn’t generating revenue. This led me to reconsider my existing infrastructure. I already host my blog, and this new link service, also based on Node.js, requires an admin panel for link management. The idea of consolidating these services onto a single server emerged as a way to reduce expenses and explore the intricacies of file hosting within Node.js.

However, cost savings aren’t the only reason to serve a Single Page Application (SPA) from your Express server.

Perhaps you’re deploying to a more traditional server environment that demands regular patching and maintenance. In such cases, minimizing the infrastructure footprint requiring this level of attention becomes crucial. (Confession: this is also a significant motivator for me.)

Alternatively, you might prefer your SPA to be accessible at your-domain.tld/app rather than app.your-domain.tld. Achieving the former is straightforward when your API serves the SPA, a process we will explore step-by-step.

Understanding the Basics: Anatomy of an Express Application

One of the primary reasons I chose Node.js and Express for my API development is the simplicity of setting up a functional web server. (And, between us, I’ve developed a real fondness for JavaScript, though don’t tell my colleagues – reputation and all that! 🤫)

Here’s a remarkably simple Express server example. Sending a GET request to the /ping endpoint will return a pong message, verifying the server’s operational status.

As you can see in the streamlined folder structure below, the demo’s organization is intentionally flat for clarity.

To keep this guide concise, I’m assuming some prior knowledge of Node.js and Express. In essence, Express applications are constructed using a series of middlewares that process requests sequentially – and this sequence is critical. Endpoint matching follows the same principle: the first route that satisfies the incoming request is executed, regardless of whether a more specific route is defined later in the configuration.

Enhancing Functionality with Middleware

The inherent nature of middleware order might tempt developers to add all routes directly to the main Express server file (typically index.js) to meticulously control execution sequence. However, this approach can quickly lead to code that is difficult to manage. A more scalable solution is to organize major routes using the Express Router. For instance, suppose we need to incorporate “admin” routes for our new admin portal. We can achieve this by instructing our application to delegate requests starting with /admin to a separate file dedicated to admin route handling.

This method promotes code separation into logical modules, simplifying the process of adding or removing admin routes without constant refactoring of the main application configuration file. But how do we define a router in a separate file and specify its routes?

The code here is designed to be quite self-explanatory. We instantiate a new Express Router, define a route, and manage the request similarly to how we would within our index.js file. The distinction is that instead of appending the get operation to the main app instance, we append it to the router. Given that we are serving an SPA, and a defining characteristic of SPAs is their self-handling of routing, we establish a rule that matches any sub-route under /admin and serves the SPA’s index.html page. In typical SPA deployments, this routing is handled by a reverse proxy or web server configuration. Now, Express enables us to manage this routing directly within our application. Note that we are serving an index.html file from a directory named admin-client, expected to reside in the application’s working directory (typically the project root).

You’ll notice that route definitions within the controller don’t require the /admin prefix. This is because we’ve already specified the router’s mount point in our index.js file. Should we decide to relocate the SPA to /app instead of /admin, a simple one-line change in index.js is all that’s needed, highlighting another advantage of using the Express Router in your projects.

Assuming your SPA is built and placed into the admin-client folder, navigating to http://localhost:3000/admin in your browser might initially result in a blank page and browser console errors. We’re still missing a crucial step. Currently, every request to /admin/* is met with our index.html page. Need a CSS stylesheet? Here’s index.html! Favicon? More index.html! While we’ve configured routing to serve our pages, we haven’t yet enabled the serving of static content. Thankfully, Express provides built-in functionality for this.

Preceding our router definition, we introduce another middleware configuration. On the same route path, but this time employing the express.static middleware. As you might deduce, this middleware handles static file serving. When a request is made for a resource under /admin, it first passes through the express.static middleware. This middleware attempts to locate a matching file within the admin-client directory. If found, it serves the file. If not, the request falls through to our admin controller. Restarting your Express server and refreshing your browser should now correctly render your SPA.

To illustrate the middleware execution flow, if you were to reverse the order of express.static and router implementations, you would encounter the same issue as before adding the express.static middleware. All requests to /admin/* would be intercepted by the router middleware, consistently returning index.html. This is why the order of declaration, with express.static preceding the router, is essential.

Final Thoughts on Express SPA Serving

For API-centric applications, or scenarios prioritizing cost efficiency, serving your SPA with Express can be an excellent solution. However, it’s important to consider long-term scalability implications compared to, say, CDN-based content delivery. The Express documentation also recommends deploying production workloads behind a reverse proxy. This best practice allows the proxy to handle tasks like caching the index.html file and leverage its strengths in areas where Express is not optimized.

One aspect I’m still refining, as my project matures, is the optimal build and deployment strategy. Initially, client and server repositories were separate, reflecting their independent treatment. Realistically, the process involves building the SPA, integrating it into the Express server, and then publishing the combined application. This suggests exploring a monorepo setup or another approach to streamline this workflow.

Furthermore, with plans to integrate my blog into this server, containerization might be on the horizon. Stay tuned for updates as this service evolves, and I can share more insights from this ongoing experience.

Leave a Reply

Your email address will not be published. Required fields are marked *