Serving headers and status codes in Angular Universal
In this article, we're going to look at how we can communicate with the Angular Express engine, allowing us to read and write HTTP headers on the server.
Why are HTTP status codes important?
We've all seen HTTP status codes, whether we recognise them or not - the classic 404 is a great example, and whilst the non-technically trained have come to learn what some of these are, for the developer community they're essential.
Not only do they handle things such as redirects, they're your SEO bread and butter:
200 tells search engines that the page is good.
301 tells search engines that pages have moved.
404 tells search engines not to index your pages.
Due to the rise in popularity of the JAMstack, Server Side Rendering has come back into fashion, with frameworks such as NextJS an Angular Universal capable of doing just this.
Where in NextJS serving a 404 - Page Not Found error code is as simple as returning notFound: true
in your getStaticProps()
method, Angular has a somewhat different approach, and of course it's not well documented.
The Angular Way
Sending HTTP Headers in the Single Page Application
The challenge we have is that somehow we need Angular to communicate with Express, and at first it's a bit of a head scratcher - how do we get a Node application that think's it's a single page application to communicate with Express?
Thankfully the team at @nguniversal/express-engine
have us covered, they've recently started providing @Inject
tokens which allow us to access the express request
and response
.
import {RESPONSE} from '@nguniversal/express-engine/tokens'; import {Inject, Injectable, Optional} from '@angular/core'; import type {Response} from 'express'; @Injectable({providedIn: 'root'}) export class ServerResponseService { constructor( @Inject(RESPONSE) private response: Response, ) {} }
The Client side of things
It's great now having access to Express, but next problem to solve is that these injection tokens are only going to be present for the Server Side Render, once we hit client land they're going to fail, and Angular will throw an error.
So how do we solve that one? Well that one's a bit easier than digging around in the nguniversal
github repo thankfully, as Angular provides us with an @Optional
decorator allowing us to gracefully proceed should they fail to inject.
Note: When using
@Optional
you'll need to check that your injected dependency is present every time you use it.
import {RESPONSE} from '@nguniversal/express-engine/tokens'; import {Inject, Injectable, Optional} from '@angular/core'; import type {Response} from 'express'; import type {ServerResponse} from 'http'; @Injectable({providedIn: 'root'}) export class ServerResponseService { constructor( @Optional() @Inject(RESPONSE) private response: Response, ) {} status(code: ServerResponse['statusCode']): void { this.response?.status(code); } }
Seamless Integration
Using Catch All Routing and an Auth Guard to set headers automatically.
Now we've got the ability to communicate with Express, we need to find where best to do the communication.
We've got a few options:
In an
ngOnInit
function within anot-found
page.This runs risk of duplication if you have different pages.
HTTP Status logic doesn't feel presentationally relevant
Within the Angular router
The Angular router supports
guards
andresolvers
which run before your page component is routed to.Angular
resolvers
are designed to pass data to your page component. wereguards
are designed to simply return whether a route is allowed or not.
Given both of these options, I find guards
fit the bill best, not-only do they ensure state of the page component by not attempting to send data, you can also configure whether they run on routes using CanActivate
, or on child routes using CanActivateChild
.
import {Injectable} from '@angular/core'; import {CanActivate} from '@angular/router'; import {ResponseService} from '../services/server-response.service'; @Injectable({ providedIn: 'root', }) export class NotFoundGuard implements CanActivate { constructor(private response: ResponseService) { } canActivate(): true { this.response.status(404); return true; } }
By using a simple guard like above, we can very easily attach it to our Angular catch all routing using the canActivate
option within the Route
.
Remember that the guard must always return true
otherwise Angular wont present your page component.
import {NgModule} from '@angular/core'; import {Route, RouterModule, Routes} from '@angular/router'; import {NotFoundGuard} from './common/guards/not-found.guard'; const home: Route = { path: '', pathMatch: 'full', loadChildren: () => import('./pages/home/home.module') .then(mod => mod.HomeModule), } const notFound: Route = { path: '**', canActivate: [NotFoundGuard], loadChildren: () => import('./pages/not-found/not-found.module') .then(mod => mod.NotFoundModule), } const routes: Routes = [ home, notFound, ]; @NgModule({ imports: [RouterModule.forRoot(routes, {initialNavigation: 'enabledBlocking'})], exports: [RouterModule], }) export class AppRoutingModule { }
Now whenever a request comes into the Angular Universal Server, Angular determine that the page doesn't exist, it'll fall into the catchAll **
route which executes the guard
, which sets the status code.
It's important to highlight that this will only serve a 404 status code for HTTP requests as client side routing doesn't return HTTP status codes.
You can find a working example of this in my Angular Universal repository on github
geometricpanda/angular-universal