Angular Server-Side Rendering(SSR): The Browser Is Not The Server
Tags: angular, typescript, javascript, ssr
Published: 2020-11-12 13:18:33

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/0atmqacjr05tp3z8crir.png)

One of the great things about **SSR** is that we get to use the same code on our **frontend and our backend to render our app**. Well, sort of.

When we use the same code, right off the bat we have a problem: **the browser is not the server** and there are differences between what we can do in each environment.

The benefit of rendering our Angular app on the server is that we can fetch data **privately and efficiently** before we send anything to the browser.

Our server is (in this case) Node.js, and so on the server we can use:

* request to make HTTP requests retrieved by the server (and these can be private authenticated requests)
* fs to access the filesystem (if we need to)
access to anything else you'd want on the server: Redis, AWS services, the database etc.

But the browser is not the server. And if we try to call browser-only APIs, then we'll break SSR.

###What can break SSR?

Well, three things come to mind that is exclusive to the browser:

* the window can be used to e.g. display alerts to the user
* the document belongs to the window namespace and is used to manipulate DOM elements
* navigator belongs to the window namespace and enables service workers that are used extensively with Progressive Web Applications

While it's awesome that our Angular application can share code between the server and the browser, if we want to use any of these objects, we need to execute a different logic path based on the current runtime: Node.js or the browser window.

Below, I'm going to show you one of the techniques for doing that

###Adding internationalization

Let's add internationalization to your application. Let's display product prices in three currencies: US dollars, British pounds, and Polish zloty. The application should pick a currency based on browser settings, and if a given language is not supported, it should fall back to Polish zloty

Let's generate a new service:

`ng g s sample`

Now let's detect user language and implement the getCurrencyCode() method that returns one of the three available currency codes:

```@Injectable({
  providedIn: 'root'
})
export class SampleService {

  private userLang;

  constructor() { 
      this.userLang = window.navigator.language;
  }

  public getCurrencyCode(): string {
    switch(this.userLang) {
      default: 
      case 'pl-PL': return 'PLN';
      case 'en-US': return 'USD';
      case 'en-EN': return 'GBP';
    }
  }
}
```

Now in one of our components, say ProductDetailsComponent, we can use this service to get the user's currency:

```
public userCurrency: string = this.sampleService.getCurrencyCode();

constructor(
  private route: ActivatedRoute, 
  private ps: ProductsService, 
  private us: UserService, 
  private sampleService: SampleService
) { }
```

Then we could use the userCurrency in a view with the currency pipe:

`<pclass="text-muted">{{userCurrency}}</p>`

From now on, prices should display in a currency defined by the user's localization settings. This is great, right?

###Well, no. Unfortunately, this logic breaks SSR:

`ERROR: ReferenceError: window is not defined`

It would help if we had a mechanism to detect whether the current runtime is the browser or the server - and thankfully that's why we have isPlatformBrowser() and isPlatformServer():

###isPlatformBrowser() and isPlatformServer()

Angular ships with the isPlatformBrowser() and isPlatformServer() methods in the @angular/common package. Each of these methods accepts one parameter: the platform ID. It can be retrieved via the Dependency Injection mechanism using the injection token PLATFORM_ID available in the @angular/core package.

So to change our internationalization service I18nService above, Add these new imports:

```
import { 
  Injectable, 
  Inject, 
  PLATFORM_ID 
  } from '@angular/core';
import { 
  isPlatformBrowser 
  } from '@angular/common';
```

Modify the service constructor to only use the window object if an instance of the service executes in the browser:

```
export class SampleService {
  constructor(
    @Inject(PLATFORM_ID)
    private platformId: any
  ) {
    if (isPlatformBrowser(this.platformId)) {
      this.userLang =
        window.navigator.language;
    } else {
      // server specific logic
    }
  }
  // ...
} 
```

This should be enough for SSR to start working again but we don't get internationalization pre-rendered on our server-side render -- internationalization won't appear until after the app loads.

So what we need is a way to know what language to render from the origin HTTP request to the server.

The Request object
The question now is how to retrieve information about user language on the server. Is it even possible?

Yes, it is.

When you're performing a request from the browser, the browser adds a bunch of HTTP headers that you might not usually think about.

One of these headers is Accept-Language which tells us what language the user wants!

For example, the header might come through like this: Accept-Language: en-US, en;q=0.5

###Getting Headers from the Request

Angular Universal allows you to get an object that represents an HTTP request. It's available via Dependency Injection under the REQUEST token from the @nguniversal/express-engine/tokens package. The Request object contains the following fields:

* body
* params
* headers
* cookies

So we update our imports by adding the Request object, the REQUEST injection token, and the Optional decorator

```
import { Injectable, Inject, PLATFORM_ID, Optional } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import { REQUEST } from '@nguniversal/express-engine/tokens';
import { Request } from 'express';
```

Change the constructor to inject the Request object and retrieve user language from the Accept-Language header:

```
export class SampleService {
  constructor(
    @Inject(PLATFORM_ID) private platformId: any,
    @Optional()
    @Inject(REQUEST) private request: Request
  ) {
    if (isPlatformBrowser(this.platformId)) {
      this.userLang =
        window.navigator.language;
    } else {
      this.userLang = (
        this.request.headers[
          "accept-language"
        ] || ""
      ).substring(0, 5);
    }
  }
  // ...
}
```