Angular Universal gives superpowers to traditional Angular apps such as server-side rendering (SSR) and full features using Express.js server. This allows us to implement features like returning HTTP status codes in appropriate scenarios.
In this post, I will show you how to set the HTTP status code from an Angular application with SSR.
Table of contents
Background
Let’s address why you might even want to set HTTP status code from an Angular component in the first place.
As an example, my application renders the following component when it does not recognise a URL:
import { Component } from '@angular/core';
@Component({
selector: 'app-not-found-page',
template: `
<h1>Not Found</h1>
<p>This page does not exist.</p>
`
})
export class NotFoundPageComponent {}
Code language: TypeScript (typescript)
When the component is rendered in the browser, the status code is actually 200 OK
.
Search engines would consider this response as valid and continue indexing the contents of this page.
In this case, we should be returning the correct status code: 404 Not Found
.
Another possible scenario is returning status code 401 Unauhorised
when securing pages from anonymous access.
Inject response from Express.js
Like any other server-side application, we need to modify the HTTP response to set the status code.
To do this in Angular Universal, we can inject the Response
object from Express.js server into the constructor of the component:
import { Component, Inject, Optional } from '@angular/core';
import { RESPONSE } from '@nguniversal/express-engine/tokens'
import { Response } from 'express'
@Component({
selector: 'app-not-found-page',
template: `
<h1>Not Found</h1>
<p>This page does not exist.</p>
`
})
export class NotFoundPageComponent {
constructor(
@Optional() @Inject(RESPONSE) private response: Response
) { }
}
Code language: TypeScript (typescript)
Notice the dependency is optional since Angular Universal applications run on both the server and the browser.
An optional parameter becomes null when the application is running in the browser. This means any usage of the response
should be protected with a null guard.
Setting the status code
With the response available in the component, we can implement our logic inside the OnInit
event.
We can pass in the status code to the status
method as shown below as per Response API docs.
export class NotFoundPageComponent implements OnInit {
constructor(
@Optional() @Inject(RESPONSE) private response: Response
) { }
ngOnInit() {
// Return 404 when running server side
if (this.response) {
this.response.status(404);
}
}
}
Code language: TypeScript (typescript)
Since the response can be null in the browser context, we guard against null value to avoid our component from failing.
Let’s now serve an SSR build locally to test the status code.
npm run build:ssr
npm run serve:ssr
Code language: Bash (bash)
Running the same scenario now yields 404 Not Found
response in the browser.
Full source code
Putting it all together this is how the component looks like now:
import { Component, Inject, OnInit, Optional } from '@angular/core';
import { RESPONSE } from '@nguniversal/express-engine/tokens'
import { Response } from 'express'
@Component({
selector: 'app-not-found-page',
template: `
<h1>Not Found</h1>
<p>This page does not exist.</p>
`
})
export class NotFoundPageComponent implements OnInit {
constructor(
@Optional() @Inject(RESPONSE) private response: Response
) { }
ngOnInit() {
// Return 404 when running server side
if (this.response) {
this.response.status(404);
}
}
}
Code language: TypeScript (typescript)
Unit tests
It helps to put in unit test coverage around the features of the component to act as a safety net. Tests also help document our code.
This is the full spec
file containing the implementation details for all of the tests:
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NotFoundPageComponent } from './not-found-page.component';
import { RESPONSE } from '@nguniversal/express-engine/tokens';
import { By } from '@angular/platform-browser';
describe('NotFoundPageComponent', () => {
let component: NotFoundPageComponent;
let fixture: ComponentFixture<NotFoundPageComponent>;
let response: any;
describe('client-side', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [NotFoundPageComponent],
providers: [
{
provide: RESPONSE,
useValue: null
}
]
})
.compileComponents();
fixture = TestBed.createComponent(NotFoundPageComponent);
component = fixture.componentInstance;
response = TestBed.inject(RESPONSE);
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should render elements', () => {
expect(fixture.debugElement.query(By.css("h1")).nativeElement.textContent).toEqual("Not Found");
expect(fixture.debugElement.query(By.css("p")).nativeElement.textContent).toEqual("This page does not exist.");
});
});
describe('server-side', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [NotFoundPageComponent],
providers: [
{
provide: RESPONSE,
useValue: jasmine.createSpyObj("Response", ["status"])
}
]
})
.compileComponents();
fixture = TestBed.createComponent(NotFoundPageComponent);
component = fixture.componentInstance;
response = TestBed.inject(RESPONSE);
fixture.detectChanges();
});
it('should set 404 status code on response', () => {
expect(response.status).toHaveBeenCalledOnceWith(404);
});
});
});
Code language: TypeScript (typescript)