Single Page Application (SPA) frameworks are common for new application development. We will not compare SPAs to traditional web applications; both have their pros and cons, but we will now focus on the hosting options of SPAs. For conventional web applications, all of the technologies have a common practice for hosting, but for SPAs, we have a bunch of possibilities to choose from.
Before deep-diving into our possibilities, let’s look closely at how SPAs work. When you visit an SPA-based website, you will load an HTML file with its javascript files and stylesheets during the initial request. After these assets are loaded, the user interface logic will be performed in your browser, and when it needs some data or just wants to persist a user action, it will communicate with a server mainly based on REST-like APIs.
So, putting these pieces together, we need
- a place where we can host the static assets of our SPA,
- and some kind of resource server that will serve as our backend application.
For this example, let’s assume we have an ASP.NET Core framework based backend application and don’t have to deploy our solution to multiple geo-locations. Also, we will not focus on load balancer and firewall components, as they could have their separate discussion. Our goal is to understand how we can host our Single Page Application in the Azure cloud.
1. Deploy together with the backend application
This approach is quite simple. We already have an ASP.NET Core backend application, and we can put the SPA’s static files next to our backend and let it serve those files. For example, we can put those files in a wwwroot folder next to the entry point of our backend application. In this case, we only need to install Microsoft.AspNetCore.SpaServices.Extensions NuGet package and add one line of code to make it work:
app.UseSpa(_ => { }); |
This middleware registration adds a fallback route to the middleware pipeline; if none of the preceding middleware handles the request, it will assume that this request was meant for the SPA, and serves the index.html from the wwwroot folder. We could fine-tune this middleware for development time purposes, but we will talk about this next time.
Pros:
- Easy to deploy, simple infrastructure.
- No extra infrastructure component is needed.
- The backend and frontend are on the same host.
- No CORS problem.
Cons:
- Scaling. We can face a similar problem that microservice architecture brought to life: only one part of the application becomes the bottleneck, and we only want to scale that part.
- We can only deploy the frontend and backend together (let’s assume we are not just copying the files over an FTP connection), so if just the frontend changes, we need to deploy the whole package together. (this is not a hard task, we only need to copy together the output of two build steps.)
- Handling these requests is an extra load for the backend application.
- A microservices architecture probably won’t work because there would be no central component where we could put our files; there would be many (ASP.NET Core based) backend services that act together as our backend component.
2. Deploy separately from the backend application
Deploying the frontend app with the backend is simple and easy to use, but as discussed, it has disadvantages. In a cloud-native environment, we can easily host it separately (at least logically) with a meagre extra or no cost.
Hosting the frontend and backend applications separately introduces some extra questions to answer:
- Do we want a separate domain for our frontend and backend?
- How can the frontend access the backend endpoints?
- Do we need to do anything related to CORS?
Let’s try to answer these questions first. One standard solution is to host the frontend on mydomain.com and the backend on api.mydomain.com or something similar. If we choose this approach, we must configure our backend service to accept requests from mydomain.com; otherwise, CORS errors will arise. Another possible way is to host the frontend on mydomain.com and serve the backend requests from mydomain.com/api. This can be easily done with a gateway or load balancer component. Sometimes, there is a requirement to not make the backend endpoints directly accessible from the public internet and have a proxy deployed with the frontend application. In this latter case, a separate gateway component could be better, but we can’t make this choice in some cases.
In the Azure cloud, we can deploy a simple SPA to:
- Virtual Machine
- App Service
- Static Web App
- Containerised environment (like Azure Kubernetes Service or OpenShift)
- Storage Account
- …
2.1 Virtual Machine
With a virtual machine, we have full control over our hosting environment, which also means we are responsible for maintaining the VM and everything installed on it. For example, we must update the installed packages and configure a backup.
Pros:
- Full control.
Cons:
- Many sys admin tasks.
- A bit more complicated Azure DevOps deployment pipeline (compared to the other solutions).
- Costly solution.
- Extra infrastructure component needed when scaling out (load balancer).
- Not really a cloud-native approach.
2.2 App Service
App Service is a straightforward-to-use Azure cloud component. It consists of an App Service Plan and the actual App Service. You can think of App Service Plan as a VM hosting many applications (App Services), but this is a fully managed environment. We only need to choose and set some properties; the platform will handle everything. The target workload of this service is dynamic content, but we can easily use it for static content hosting. I suggest using Linux for our hosting as it is cheaper than Windows (you need to pay for the OS).
If you choose Windows, you will have IIS as your hosting service. This means that by default, it will be able to serve the static files without any configuration, but you will need to create a web.config file next to your application and add a rewrite rule to it to make all requests that do not match an existing file fallback to your SPA’s index.html file.
The Linux option is more complex; your environment will depend on what technology you selected for your platform. For example, older PHP versions use an apache2 web server, so you can use the .htaccess file to configure it, but newer versions use Nginx, and you need to go with some hacky solutions to modify its settings. I suggest using Node.js, and you can benefit from the pm2 web server. You only need to provide the following startup command and deploy your application’s files.
pm2 serve /home/site/wwwroot –no-daemon –spa |
2.3 Static Web App
Azure Static Web Apps became generally available about two years ago, so many older tutorials do not mention this resource type. Also, during this time, it developed a lot; initially, the create wizard only supported GitHub as the source. Even for Azure DevOps, you had to create the deployment pipeline manually. Now, it has become the go-to solution. It has a free tier that is enough for personal use or smaller organisations (remember that it does not have an SLA), but even the standard plan is budget-friendly. In the creation wizard, you can select a GitHub or Azure DevOps repository as a source, and it can create a deployment pipeline for it.
Pros:
- Budget-friendly solution.
- Supports most of the well-known frontend frameworks out of the box.
- It can easily integrate with an Azure Function based backend API.
- You can use your custom domain and get a valid HTTPS certificate in the free plan. (Compared to App Service, which requires at least a Basic plan for these.)
Cons:
- It’s not a mature solution; sometimes, you can stumble into problems (but they are fixed quite fast). Previously it took a lot of work to find solutions to our issues, for example on Stack Overflow, but it evolved a lot.
2.4 Containerized environment
We can talk about many types of containerised environments. The simplest one is when we have a Docker container and want to run it in the cloud. In Azure, we can use an App Service to host an individual container. We only need to provide the image repository and the specific image name and tag. It can be in an Azure Container Registry, Docker hub, or any other container registry.
Containerising an SPA is easy to do, we can use Nginx to host the application.
Let’s discuss more complex scenarios with a container orchestrator system (like Kubernetes or OpenShift). In a microservices architecture-based system, we need one. In the Azure cloud, we can create managed or self-managed instances of these systems. If we don’t want to deal with the management of the underlying infrastructure, it is recommended to choose the managed approach. We won’t talk about provisioning an orchestrator system; it could have a separate article. So, we already have our environment, and the backend service has been deployed. How can we proceed? One of the most common solutions is containerising our frontend application’s assets next to an nginx server. This Nginx server will serve the incoming requests to our SPA.
First, we need to create an nginx.conf file:
server { listen 80; root /usr/share/nginx/html; index index.html; server_name _; # all hostnames location / { try_files $uri /index.html; } } |
And of course, we also need to create a Dockerfile:
# —- Build stage —- FROM node:alpine AS build-step RUN mkdir -p /app WORKDIR /app # Copy project files COPY . /app # Build project RUN npm install RUN npm run build # —- Prod —- FROM nginx # Copy nginx.config and SPA files COPY nginx.config /etc/nginx/conf.d/default.conf COPY –from=build-step /app/dist /usr/share/nginx/html |
The Nginx server can also proxy the API requests to the backend application, we need to add a rule in the nginx.conf file that everything that is coming to the /api path needs to be proxied to our backend service (we could even create multiple rules for the different services), but in most cases, a gateway component in the cluster is more suitable for this purpose. Depending on the infrastructure of our solution we could choose from many options: it could be an independent nginx service, in a kubernetes cluster we can leverage Ingress resources, we can use an existing gateway solution (like envoy or traefik), or self-create a gateway application (Ocelot could be a good starting point in the .NET ecosystem).
Pros:
- In a microservices architecture-based system, we have no other choice than containerising the SPA.
- We can easily move the application to an on-premises infrastructure in a cloud-agnostic solution.
Cons:
- It requires more configuration compared to other solutions.
- Not a fully cloud-native solution.