HTTP with Angular Universal

HTTP with Angular Universal is kinda a big deal 😎 have you ever thought, Ok, so Angular Universal Apps are rendered on the server and the resulting HTML is returned to the client .. hmmm, ok .. but that’s for static data.

What if, I wanna call an API, get some data from the server, and … yes, you guessed it right ! add the API response to my meta tags, What should I do 🥲

crying

As most of us know, HTTP requests happen on the browser/client, and in our case, we will write them in our Angular code, right?

Hmmm, Ok, Our Angular is a Universal App, ok it’s processed on the server and the rendered HTML is returned to the client. So, If we have an HTTP request, will it happen on the Browser? or on the Server?

i don't feel safe

Take a guess, and let me know your guess in the comments below 🤭

The Answer

The answer is, you can specify whether your HTTP should be called on the server or on the client, I will walk you through it using our repo from our previous post Angular SEO

How?

Using TransferState

Transfer state is a strategy where we:

  1. Fetch the data required to render the full “static” application using either the SSR or prerendering strategies
  2. Serialize the data, and send the data with the initial document (HTML) response
  3. Parse the serialized data at runtime when the application is initialized, avoiding a redundant fetch of the data

So, basically any HTTP request we need to make on the server, we will use transfer state for it, any HTTP request we need to be done on the client/browser, we will use our normal HttpClient

what??

I know it may be a little confusing. But, wanna jump to code to get more sense? 👨‍💻

We will use this dummy API that returns a list of products with:

https://dummyjson.com/products // Returns list of products

and

https://dummyjson.com/products/1 // Returns product details

Our goal is:

  • To make the /products API happen on the client (Because we don’t care for meta tags on our home page)
  • To make the /products/{ID} happen on the server (So that we can fetch the product details and add it to our meta tags)

Let’s Code !! 👨‍💻

We will start by creating a ProductService that calls our APIs

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Injectable({providedIn: 'root'})
export class ProductService {
    constructor(private httpClient: HttpClient) { }
}

Creating product.service.ts file on the root of our app, easy and simple 🤭

Now, let’s add two methods:

  • One for /products that fetches all the products
  • Another for /products/{ID} that fetches the product details
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable({providedIn: 'root'})
export class ProductService {
    constructor(private httpClient: HttpClient) { }

    getProducts(): Observable<any> {
        return this.httpClient.get(`https://dummyjson.com/products`);
    }

    getProductDetails(id: number): Observable<any> {
        return this.httpClient.get(`https://dummyjson.com/products/${id}`);
    }
}

Don’t forget to add HttpClientModule to your app.module.ts

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { AboutComponent } from './about/about.component';
import { ContactComponent } from './contact/contact.component';
import { HttpClientModule } from '@angular/common/http';

@NgModule({
  declarations: [
    AppComponent,
    AboutComponent,
    ContactComponent
  ],
  imports: [
    BrowserModule.withServerTransition({ appId: 'serverApp' }),
    HttpClientModule,
    AppRoutingModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Ok, now let’s add our call to our app.component.ts and app.component.html

import { Component } from '@angular/core';
import { ProductService } from './product.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'Angular SEO';

  products!: any[];

  constructor(private productService: ProductService) {
    this.productService.getProducts().subscribe(
      products => this.products = products.products
    );
  }
}

and our HTML

<h1>
  Welcome To {{title}}
</h1>
<ul>
  <li>
    <a [routerLink]="['/about']" routerLinkActive="router-link-active">About</a>
  </li>
  <li>
    <a [routerLink]="['/contact']" routerLinkActive="router-link-active">Contact</a>
  </li>
</ul>

<ul>
  <li *ngFor="let product of products">
    {{product.title}}
  </li>
</ul>

<router-outlet></router-outlet>

It will finally look like this:

browser

PERFECT !

Now, we need to create a details component that will show the product’s info when clicking on it

First, let’s convert our list of products to links, and create our details component with `ng generate component details`

<h1>
  Welcome To {{title}}
</h1>
<ul>
  <li>
    <a [routerLink]="['/about']" routerLinkActive="router-link-active">About</a>
  </li>
  <li>
    <a [routerLink]="['/contact']" routerLinkActive="router-link-active">Contact</a>
  </li>
</ul>

<ul>
  <li *ngFor="let product of products">
    <a [routerLink]="['/products/'+product.id]" routerLinkActive="router-link-active" >{{product.title}}</a>
  </li>
</ul>

<router-outlet></router-outlet>

And we will create a route for it in the app-routing.module.ts, like this:

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AboutComponent } from './about/about.component';
import { ContactComponent } from './contact/contact.component';
import { DetailsComponent } from './details/details.component';

const routes: Routes = [
  { path: 'about', component: AboutComponent },
  { path: 'contact', component: ContactComponent },
  { path: 'products/:id', component: DetailsComponent },
];

@NgModule({
  imports: [RouterModule.forRoot(routes, {
    initialNavigation: 'enabledBlocking'
})],
  exports: [RouterModule]
})
export class AppRoutingModule { }

Now the page should look like this and we will be able to navigate to the details component

browser

Now, let’s call the details API in our details component and print the products’ details

import { Component } from '@angular/core';
import { ProductService } from '../product.service';
import { ActivatedRoute } from '@angular/router';

@Component({
  selector: 'app-details',
  templateUrl: './details.component.html',
  styleUrls: ['./details.component.css']
})
export class DetailsComponent {

  product!: any;

  constructor(
    private productService: ProductService,
    private route: ActivatedRoute
    ) {
      this.route.params.subscribe(
        (params: any) => {
          this.productService.getProductDetails(params.id).subscribe(
            product => {
              this.product = product;
            }
          )
        }
      )
  }
}

and our details.component.html

{{product | json}}

the product details page now should look like this:

browser

Now, let’s add the Meta tags for each product

import { Component } from '@angular/core';
import { ProductService } from '../product.service';
import { ActivatedRoute } from '@angular/router';
import { MetaService } from '../mets.service';

@Component({
  selector: 'app-details',
  templateUrl: './details.component.html',
  styleUrls: ['./details.component.css']
})
export class DetailsComponent {

  product!: any;

  constructor(
    private productService: ProductService,
    private route: ActivatedRoute,
    private metaService: MetaService,
    ) {
      this.route.params.subscribe(
        (params: any) => {
          this.productService.getProductDetails(params.id).subscribe(
            product => {
              this.product = product;
              this.metaService.addMetaTag('og:title', product.title);
              this.metaService.addMetaTag('og:description', product.description);
            }
          )
        }
      )
  }
}

What does the page source look like? let’s press ctrl+u and check

<!DOCTYPE html>
<html lang="en">
   <head>
      <meta charset="utf-8">
      <title>Angularseo</title>
      <base href="/">
      <meta name="viewport" content="width=device-width, initial-scale=1">
      <link rel="icon" type="image/x-icon" href="favicon.ico">
      <link rel="stylesheet" href="styles.css" media="print" onload="this.media='all'">
      <noscript>
         <link rel="stylesheet" href="styles.css">
      </noscript>
      <style ng-transition="serverApp">ul[_ngcontent-sc4]   li[_ngcontent-sc4] {
         display: inline-block;
         margin: 0 10px;
         }
      </style>
      <meta name="og:title" content="iPhone X" property="og:title">
      <meta name="og:description" content="SIM-Free, Model A19211 6.5-inch Super Retina HD display with OLED technology A12 Bionic chip with ..." property="og:description">
   </head>
   <body>
      <app-root _nghost-sc4="" ng-version="15.2.9" ng-server-context="ssr">
        .
        .
        .
        .
        .
         <router-outlet _ngcontent-sc4=""></router-outlet>
         <app-details _nghost-sc3="">{
            "id": 2,
            "title": "iPhone X",
            "description": "SIM-Free, Model A19211 6.5-inch Super Retina HD display with OLED technology A12 Bionic chip with ...",
            "price": 899,
            "discountPercentage": 17.94,
            "rating": 4.44,
            "stock": 34,
            "brand": "Apple",
            "category": "smartphones",
            "thumbnail": "https://i.dummyjson.com/data/products/2/thumbnail.jpg",
            "images": [
            "https://i.dummyjson.com/data/products/2/1.jpg",
            "https://i.dummyjson.com/data/products/2/2.jpg",
            "https://i.dummyjson.com/data/products/2/3.jpg",
            "https://i.dummyjson.com/data/products/2/thumbnail.jpg"
            ]
            }
         </app-details>
         <!--container-->
      </app-root>
      <script src="runtime.js" type="module"></script><script src="polyfills.js" type="module"></script><script src="vendor.js" type="module"></script><script src="main.js" type="module"></script>
   </body>
</html>

WHAT??

We will find something weird, the meta tags are set already … how?

Did the API call happen on the server or on the client/browser?

In a matter of fact .. BOTH

Is that a good thing?

We will make an HTTP call on the browser and on the server, that’s not good because it will be redundant.

How can we prevent this from happening?

Yes, using TransferState

We will basically create a service that decides where should the api call happen.

If the API call is done on the browser and the data is fetched successfully, when the client tries to re-fetch the data it will check something called the store for it, if it finds the data, it will get it from the store without recalling the API

Let’s create a service called data.service.ts

import { Injectable, PLATFORM_ID, Inject } from '@angular/core';
import { TransferState, makeStateKey } from '@angular/platform-browser';
import { Observable, of } from 'rxjs';
import { isPlatformServer } from '@angular/common';

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

  private isServer = false;

  constructor(
    private tstate: TransferState,
    @Inject(PLATFORM_ID) platformId: Object,
  ) {
    this.isServer = isPlatformServer(platformId);
  }
  
  checkAndGetData(key: string, observable: Observable<any>) {
    let keyState = makeStateKey<any>(key);
    if (this.tstate.hasKey(keyState) && !this.isServer) {
      return of(this.tstate.get(keyState, []));
    } else {
      return observable;
    }
  }

}

And, we will change our service calls to:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { DataStateService } from './data.service';

@Injectable({providedIn: 'root'})
export class ProductService {
    constructor(
        private httpClient: HttpClient,
        private dataSevrvice: DataStateService    
    ) { }

    getProducts(): Observable<any> {
        return this.dataSevrvice.checkAndGetData('products', this.httpClient.get(`https://dummyjson.com/products`));
    }

    getProductDetails(id: number): Observable<any> {
        return this.dataSevrvice.checkAndGetData(`product${id}details`, this.httpClient.get(`https://dummyjson.com/products/${id}`));
    }
}

And modify our details component to:

import { Component } from '@angular/core';
import { ProductService } from '../product.service';
import { ActivatedRoute } from '@angular/router';
import { MetaService } from '../mets.service';
import { TransferState, makeStateKey } from '@angular/platform-browser';

@Component({
  selector: 'app-details',
  templateUrl: './details.component.html',
  styleUrls: ['./details.component.css']
})
export class DetailsComponent {

  product!: any;

  constructor(
    private productService: ProductService,
    private route: ActivatedRoute,
    private tstate: TransferState,
    private metaService: MetaService,
    ) {
      this.route.params.subscribe(
        (params: any) => {
          this.productService.getProductDetails(params.id).subscribe(
            product => {
              let keyState = makeStateKey<any>(`product${product.id}details`);
              this.tstate.set(keyState, product);
              this.product = product;
              this.metaService.addMetaTag('og:title', product.title);
              this.metaService.addMetaTag('og:description', product.description);
            }
          )
        }
      )
  }
}

Now, Our calls will be done only on the server 😎😎😎

If we want to make our calls to ONLY happen on the browser, we can do check if the platformId is client.

Let’s modify the products call in app.component.ts to:

import { Component } from '@angular/core';
import { ProductService } from './product.service';
import { Injectable, PLATFORM_ID, Inject } from '@angular/core';
import { TransferState, makeStateKey } from '@angular/platform-browser';
import { isPlatformBrowser } from '@angular/common';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'Angular SEO';

  products!: any[];

  constructor(private productService: ProductService,
    @Inject(PLATFORM_ID) platformId: Object,
    private tstate: TransferState) {
      if (isPlatformBrowser(platformId)) {
        this.productService.getProducts().subscribe(
          products => {
            let keyState = makeStateKey<any>(`products`);
            this.tstate.set(keyState, products);
            this.products = products.products;
          }
        );
      }
  }
}

Now, the /products api call happens on the browser/client only, and the product details /products/{ID} call happens on the server only.

REPO

You will find all this code pushed to this GitHub Repo 🚀

As always, I hope it was beneficial to you, and as always as well, if it wasn’t … you will always find a potato at the end.

potato