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.

server-response.service.ts
ts
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.

server-response.service.ts
ts
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:

  1. In an ngOnInit function within a not-found page.

    • This runs risk of duplication if you have different pages.

    • HTTP Status logic doesn't feel presentationally relevant

  2. Within the Angular router

    • The Angular router supports guards and resolvers which run before your page component is routed to.

    • Angular resolvers are designed to pass data to your page component. were guards 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.

not-found.guard.ts
ts
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.

app-routing.module.ts
ts
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