How to Serve Up an SPA with Nginx
Learn how to serve up an SPA with Nginx. We will also go over what a single page application is and how to use the Nginx try_files directive.
Table of Contents 📖
What is an SPA?
To understand how to serve up an SPA app with Nginx, we first need to understand what an SPA, or single page application, is. An SPA is a web app that consists of only a single web document, usually an index.html file, and some other assets such as a JavaScript file, images, fonts, etc. For example, below is a typical SPA structure that can be distributed on the web.
[object Object]
- index.html - The HTML document that houses the application.
- bundle.js - The JavaScript file that renders the application in the HTML document. For example, it could be bundled React or Angular code.
- main.css - The CSS file that styles the application.
- logo.png - Image used in the application.
The body of the index.html document is changed by using JavaScript. Therefore, unlike multi page applications, requests to multiple routes should return the same HTML document. For example, /about should return an index.html file, /contact should return the index.html file, etc. The contents of the HTML file will be changed using JavaScript.
INFO: In a multipage app, requests to /about would return about.html, /contact would return contact.html, etc.
The JavaScript will most likely be requested by the index.html file using the script tag and the CSS by using a link tag.
<link rel="stylesheet" href="main.css">
<script defer="defer" src="bundle.js"></ script>
Nginx and SPAs
This means that we need to configure Nginx to return the SPA HTML file on each route, unless another document exists at that route of course. We can do this using the try_files directive.
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '[$time_local] "$request" URI - $uri';
access_log /var/log/nginx/access.log main;
sendfile on;
keepalive_timeout 65;
server {
listen 80;
listen [::]:80;
server_name localhost;
root /usr/share/nginx/html;
location / {
try_files $uri /index.html;
}
}
}
The try_files directive tries to return the path specified in relation to the root directive. In this case, the root directive is /usr/share/nginx/html. Therefore, a request to /about would return /usr/share/nginx/html/about and a request to /contact would return /usr/share/nginx/html/contact. However, we have Nginx configured here so that if the file does not exist, try_files will return the index.html file instead.
INFO: The root directive sets the root directory for requets. A path to a file is constructed by adding a URI to the value of the root directive.
Specifically, the try_files directive checks the existence of the provided files in the specified order and uses the first found file to process the request. The way we have it set up, the index.html file will always be returned if the requested URI does not exist.
Checking Access Logs
If we look at the access logs, we can see the requested URI and what is returned. The access_log and log_format directives determine what the access logs look like.
log_format main '[$time_local] "$request" URI - $uri';
access_log /var/log/nginx/access.log main;
- $request - The full original request line.
- $time_local - The local time of the request in Common Log Format.
- $uri - The current URI in the request.
Here are the outputted logs.
[15/May/2024:11:29:13 +0000] "GET / HTTP/1.1" URI - /index.html
[15/May/2024:11:29:15 +0000] "GET /hi HTTP/1.1" URI - /index.html
[15/May/2024:11:29:43 +0000] "GET /hello HTTP/1.1" URI - /index.html
[15/May/2024:11:29:45 +0000] "GET /wittcode HTTP/1.1" URI - /index.html
[15/May/2024:11:29:48 +0000] "GET /index.html HTTP/1.1" URI - /index.html
[15/May/2024:11:29:51 +0000] "GET /about HTTP/1.1" URI - /index.html
[15/May/2024:11:29:53 +0000] "GET /contact HTTP/1.1" URI - /index.html
Notice how every request receives the index.html file in response. This means that when a user makes a request to /about they would be provided the index.html file and then the JavaScript would render the about page inside the index.html file.