Search code examples
angularangular-universalangular-ssr

Angular Universal SSR API Calling two time one from Frontend and Second fro backend


I am trying to achieve SSR using Angular Universal, but facing two problem

  1. I getting two hit on my server as i can see in server logs, one from frontend and second from backend.

  2. when universal sever is calling the api, not injecting data to component to bind the data.

my package.json

{
  "name": "web-client",
  "version": "0.0.0",
  "scripts": {
    "ng": "ng",
    "start": "ng serve",
    "build": "ng build --prod",
    "watch": "ng build --watch --configuration development",
    "test": "ng test",
    "dev:ssr": "ng run web-client:serve-ssr",
    "serve:ssr": "node dist/web-client/server/main.js",
    "build:ssr": "ng build && ng run web-client:server",
    "prerender": "ng run web-client:prerender",
    "build:stats": "ng build --stats-json",
    "analyze": "webpack-bundle-analyzer dist/web-client/browser/stats.json"
  },
  "private": true,
  "dependencies": {
    "@angular/animations": "^15.0.0",
    "@angular/cdk": "^15.0.3",
    "@angular/common": "^15.0.0",
    "@angular/compiler": "^15.0.0",
    "@angular/core": "^15.0.0",
    "@angular/forms": "^15.0.0",
    "@angular/material": "^15.0.3",
    "@angular/platform-browser": "^15.0.0",
    "@angular/platform-browser-dynamic": "^15.0.0",
    "@angular/platform-server": "^15.0.0",
    "@angular/router": "^15.0.0",
    "@auth0/auth0-angular": "^2.0.1",
    "@nestjs/common": "^9.3.2",
    "@nestjs/core": "^9.3.2",
    "@nguniversal/express-engine": "^15.0.0",
    "express": "^4.15.2",
    "http-proxy-middleware": "^2.0.6",
    "ngx-spinner": "^15.0.1",
    "rxjs": "~7.5.0",
    "tslib": "^2.3.0",
    "zone.js": "~0.12.0"
  },
  "devDependencies": {
    "@angular-devkit/build-angular": "^15.0.4",
    "@angular/cli": "~15.0.4",
    "@angular/compiler-cli": "^15.0.0",
    "@nguniversal/builders": "^15.0.0",
    "@types/express": "^4.17.0",
    "@types/jasmine": "~4.3.0",
    "@types/node": "^14.15.0",
    "jasmine-core": "~4.5.0",
    "karma": "~6.4.0",
    "karma-chrome-launcher": "~3.1.0",
    "karma-coverage": "~2.2.0",
    "karma-jasmine": "~5.1.0",
    "karma-jasmine-html-reporter": "~2.0.0",
    "typescript": "~4.8.2",
    "webpack-bundle-analyzer": "^4.7.0"
  }
}

and server.ts file


import 'zone.js/node';

import { APP_BASE_HREF } from '@angular/common';
import { ngExpressEngine } from '@nguniversal/express-engine';
import * as express from 'express';
import { existsSync } from 'fs';
import { join } from 'path';
const { createProxyMiddleware } = require('http-proxy-middleware');
import { AppServerModule } from './src/main.server';

// The Express app is exported so that it can be used by serverless Functions.
export function app(): express.Express {
  const server = express();
  const distFolder = join(process.cwd(), 'dist/b-jobz-web-client/browser');
  const indexHtml = existsSync(join(distFolder, 'index.original.html')) ? 'index.original.html' : 'index';



  const options = {
    target: 'http://localhost:8080', // target host
    pathRewrite: {
      '^/api': ''
    },
    logLevel: 'debug',
  };

  server.use(
    '/staging',
    createProxyMiddleware({
      target: 'http://localhost:3002',
      changeOrigin: true,
      pathRewrite: {
        '^/api': ''
      },
      logLevel: 'debug',
    })
  );


  // Our Universal express-engine (found @ https://github.com/angular/universal/tree/main/modules/express-engine)
  server.engine('html', ngExpressEngine({
    bootstrap: AppServerModule,
  }));

  server.set('view engine', 'html');
  server.set('views', distFolder);

  server.get('*.*', express.static(distFolder, {
    maxAge: '1y'
  }));

  // All regular routes use the Universal engine
  server.get('*', (req, res) => {
    res.render(indexHtml, { req, providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }] });
  });

  return server;
}

function run(): void {
  const port = process.env['PORT'] || 4000;

  // Start up the Node server
  const server = app();
  server.listen(port, () => {
    console.log(`Node Express server listening on http://localhost:${port}`);
  });
}

// Webpack will replace 'require' with '__webpack_require__'
// '__non_webpack_require__' is a proxy to Node 'require'
// The below code is to ensure that the server is run only when not requiring the bundle.
declare const __non_webpack_require__: NodeRequire;
const mainModule = __non_webpack_require__.main;
const moduleFilename = mainModule && mainModule.filename || '';
if (moduleFilename === __filename || moduleFilename.includes('iisnode')) {
  run();
}

export * from './src/main.server';

API call should be called from backend and inject the response to angular component to achieve SSR.


Solution

  • The main reason for such case is how angular universal works. It will render your page on the server and send it to browser. Once it reaches the browser, the angular portion kicks in and starts running. That is you api calls and other i/o will take place again. That is how it works. But it can be avoided using TransferState api. This can be implied in many ways. A popular way can be using it in interceptors. The logic shall work like following.

    You will fetch the data on the server and save it using transferstate api and in browser interceptor you can check the transferstate api for the data and return that as response stopping the browser from making any call. The code shall work like following

    In you app server modules ts add

      providers: [
         {
           provide: HTTP_INTERCEPTORS,
           useClass: ServerStateInterceptor,
           multi: true
         },
      ],
    

    In your app module add

      providers: [
         {
           provide: HTTP_INTERCEPTORS,
           useClass: BrowserStateInterceptor,
           multi: true
         },
     ]
    

    Now in your server interceptor

     intercept(req: HttpRequest<any>, next: HttpHandler) {
            return next.handle(req).pipe(
              tap(event => {
                if(req.method==='POST' || req.method==='GET'){
        
                  if ((event instanceof HttpResponse && (event.status === 200 || event.status === 202))) {
                    let key: any="";
                    if(req.url!==null){
                      key=req.url
                    }
                    this.transferState.set(makeStateKey(key), event.body);
                  }
                }
              }),
            );
          }
    

    Now you just get the data from transfer state in your browser interceptor

     intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        if (req.method === 'POST'|| req.method === 'GET') {
            let postKey: any="";
            if(req.url!==null){
              postKey = req.url as string;
            }
            const key = makeStateKey(postKey);
            const storedResponse = this.transferState.get(key, null);
            if (storedResponse) {
              const response = new HttpResponse({body: storedResponse, status: 200});
              return of(response);
            }
        }
      }
    

    This will stop your app from calling the server twice. You may implement other types of implementation such as using resolvers. It depends on your architecture. But more or less this will solve your case.