Get Your Frontend off My Lawn

With modern web applications, we often split the frontend and backend into components that can be developed separately. So we develop each piece and when we go to deploy the final product you have to decide whether to deploy each piece to a separate domain or combine the two into one deployment.

Using different domains is probably the easiest for not having to modify your code to use it in production. With this method, the first problem you’ll encounter is having to account for CORS headers (Cross-Origin Resource Sharing) in your backend code. There are many libraries to help you with that task. The second problem is having to setup and possibly pay for two different deployments. For smaller applications and teams or in certain situations, deploying everything together can be preferable. This is doable but can seem kind of weird for seasoned developers.

What is the cause of all the weirdness? When we combine the frontend and backend in a single page application (SPA), the backend must serve up all of its usual API routes and static files but then any unmatched request gets served to index.html instead of showing a 404. It looks something like this:

Incoming Request ->
1. Does it match an API route?
2. Does it match a static file path?
3. No match, then return index.html
(additional routing is now handled by frontend code)

Because the frontend includes URL routing with via the browser’s History Push State, it is up to the frontend to have the final word on unmatched URLs. So you will not get a proper 404 status code with this method. To the end user this is not a big deal because you can still show “Not Found” pages. But you’ll probably want to be more careful about SEO and where you direct search engines to scrape your site if you move content.

Backend Implementations for SPA Routing

If combining the two components is for you, I wanted to show you how this can be done in several different frameworks. Again this isn’t necessarily the best method, but does have advantages in certain use cases. Some cases where I like to use this method:

  • I don’t want to setup two deployments (small team, small app)
  • The application is installed locally by a user and must run under one domain like localhost.

Nginx

If you have Nginx running in front of your code, you can use the web server to handle your initial routing so you don’t have to make modifications to your code. A simplified configuration is below.

server {
# Backend routes
location /api/ {
proxy_pass http://localhost:8000;
# probably more stuff here
}

# Static Files (optional)
location /static/ {
root /path/to/static/files;
}
  # Frontend 
location / {
root /path/to/frontend/build;
try_files $uri /index.html;
}
}

Note the try_files directive. That tells Nginx to try and match the URL to a static file and if not fallback to index.html.

ExpressJS

// optional static files
app.use('/static', express.static('path/to/static/files'));
// frontend build
app.use(express.static('path/to/build'));
// Backend routes
// Catch all for frontend
app.get('*', function (req, resp) {
resp.sendFile(_dirName + '/path/to/build/index.html');
});

Tornado (Python)

class FrontendHandler(tornado.web.StaticFileHandler):
def validate_absolute_path(self, root, absolute_path):
try:
return super().validate_absolute_path(root, absolute_path)
    except tornado.web.HTTPError as exc:
if exc.status_code == 404 and \
self.default_filename is not None:
absolute_path = self.get_absolute_path(
self.root, self.default_filename)
return super().validate_absolute_path(root, absolute_path)
      raise exc
def make_app():
return tornado.web.Application([
# Backend routes

# Optional static files
(r'/static/(.*)', tornado.web.StaticFileHandler, {
'path': '/path/to/static/files'}),
    # Frontend
(r'/(.*)', FrontendHandler, {
'path': '/path/to/frontend/build',
'default_filename': 'index.html'}),
])

Django

With the Django setup I’m assuming you are already using something like WhiteNoise. This setup serves the frontend index.html as a Django template. You could get rid of that and the TEMPLATES settings change; however, I like to do it this way to inject variables that need to be changed for production use.

settings.py

TEMPLATES = [
{
# Other Template settings
'DIRS': [
'/path/to/frontend/build'
],
}
]
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, "static-compiled")
STATICFILES_DIRS = [
'/path/to/frontend/build'
]

urls.py

urlpatterns = [
# Backend URLs
  re_path(r'(.*)', main.views.frontend),
]

main/views.py

@cache_page(CACHE_TIMEOUT)
def frontend(request):
return TemplateResponse(request, 'index.html', {})

If you would like to add your framework to this list, let me know on Twitter: PizzaPanther.