Replacing Angular Universal with SSR version 17.0
Tags: angular, webdev, tutorial, typescript
Published: 2024-01-23 10:00:00

Angular has adopted Universal and made it a kid of its own! Today I will rewrite some old code we used to [isolated express server](https://garage.sekrab.com/posts/loading-external-configurations-in-angular-universal#Isolating-the-server) as we documented before with the most extreme version of [serving the same build for multiple languages from Express](https://garage.sekrab.com/posts/serving-the-same-angular-build-with-different-urls), we are also using a [standalone version](https://garage.sekrab.com/posts/angular-standalone-in-ssr-update) as we covered recently.

Let's dig.

> Find GitHub branch of [cricketere](https://github.com/ayyash/cricketere/tree/a17) with server implementation

## Setup

We need to first install **SSR **package and remove any reference to **Angular Universal**. (Assuming we have upgraded to Angular 17.0)

`npm install @angular/ssr`

`npm uninstall @nguniversal/common @nguniversal/express-engine @nguniversal/builders`

Using `ng add @angular/ssr` rewrites the `server.ts` and it claims to add a config file somewhere. Yeah, nope!

In our last attempt for a skimmed down, standalone `server.ts`, it looked like this

```ts
// the last server.ts
import { ngExpressEngine } from '@nguniversal/express-engine';
import 'zone.js/dist/zone-node';
// ...
const _app = () => bootstrapApplication(AppComponent, {
  providers: [
    importProvidersFrom(ServerModule),
    // providers: providers array...
  ],
});

// export the bare minimum, let nodejs take care of everything else
export const AppEngine = ngExpressEngine({
  bootstrap: _app
});
```

Now the `ngExpressEngine` is gone. Here are three sources to dig into to see if we can create our own skimmed down engine.

-   [ngExpressEngine source code](https://github.com/angular/universal/blob/main/modules/express-engine/src/main.ts#L33)
-   The new ssr [CommonEngine source code](https://github.com/angular/angular-cli/blob/main/packages/angular/ssr/src/common-engine.ts)
-   Angular [documentation of the ssr CommonEngine](https://angular.io/guide/ssr#configure-server-side-rendering)
-   [Express custom template engine](https://expressjs.com/en/advanced/developing-template-engines.html) documentation

The original Universal Express Engine received `options`, created the return function, and used the `CommonEngine` to `render`. We can recreate a template engine with much less **abstraction** in our `server.ts`. Here is the outcome.

## Changes

-   First, remember to import `zone.js` directly instead of the deep path `zone.js/dist/zone-node`
-   The `CommonEngine` instance can be created directly with at least `bootstrap` property set
-   Export the **express engine** function
-   Get rid of a lot of abstractions
-   The URL property cannot be set in Angular, it better be set in Express route.
-   Use `provideServerRendering`, instead of `importProvidersFrom`

The new `server.ts` now looks like this:

```ts
// server.ts
// remove nguniversal references
// import { ngExpressEngine } from '@nguniversal/express-engine';

// change import from deep to shallow:
import 'zone.js';

// add this
import { CommonEngine, CommonEngineRenderOptions } from '@angular/ssr';

// the standalone bootstrapper
const _app = () => bootstrapApplication(AppComponent, {
  providers: [
    // this new line from @angular/platform-server
    provideServerRendering(),
    // provide what we need for our multilingual build
    // ... providers array
  ],
});

// create engine from CommonEngine, pass the bootstrap property
const engine = new CommonEngine({ bootstrap: _app });

// custom express template angine, lets call it cr for cricket
export function crExpressEgine(
  filePath: string,
  options: object,
  callback: (err?: Error | null, html?: string) => void,
) {
  try {
    // grab the options passed in our Express server
    const renderOptions = { ...options } as CommonEngineRenderOptions;

    // set documentFilePath to the first arugment of render
    renderOptions.documentFilePath = filePath;

    // the options contain settings.view value
    // which is set by app.set('views', './client') in Express server
    // assign it to publicPath
    renderOptions.publicPath = (options as any).settings?.views;

    // then render
    engine
      .render(renderOptions)
      .then((html) => callback(null, html))
      .catch(callback);
  } catch (err) {
    callback(err);
  }
};
```

Our Express route code, which is where the main call for all URLs are caught and processed:

```js
// routes.js
// this won't change (it's defined by outputPath of angular.json server build)
const ssr = require('./ng/main');

// app is an express app created earlier
module.exports = function (app, config) {
  // we change the engine to crExpressEngine
  app.engine('html', ssr.crExpressEgine);
  app.set('view engine', 'html');
  // here we set the views for publicPath
  app.set('views',  './client');

  app.get('*'), (req, res) => {
    const { protocol, originalUrl, headers } = req;

    // serve the main index file
    res.render(`client/index.html`, {
      // set the URL here
      url: `${protocol}://${headers.host}${originalUrl}`,
      // pass providers here, if any, for example "serverUrl"
      providers: [
        {
          provide: 'serverUrl',
          useValue: res.locals.serverUrl // something already saved
        }
      ],
      // we can also pass other options
      // document: use this to generate different DOM content
      // turn off inlinecriticalcss
      // inlineCriticalCss: false
    });
  });
};
```

Built, Express server run, and tested. Works as expected.

A note about the URL, in a [previous article](https://garage.sekrab.com/posts/loading-external-configurations-in-angular-universal#The-curious-case-of-reverse-proxy) we ran into an issue with reverse proxy, and had to set the the URL from a different source, as follows:

```js
// fixing URL with reverse proxy
let proto = req.protocol;
if (req.headers && req.headers['x-forwarded-proto']) {
    // use this instead
    proto = req.headers['x-forwarded-proto'].toString();
}
// also, always use req.get('host')
const url = `${proto}://${req.get('host')}`;
```

This is better than the one documented in Angular.

## Passing request and response

We cannot provide `req` and `res` as we did before, we used to depend on `REQUEST` token from `nguniversal` library. But we don't need to most of the time, we can inject the values we want from request directly into the express providers array. Here are a couple of examples: a `json` config file from the server, and the server URL:

```js
// routes.js
//...
res.render(...,
  // ... provide a config.json added in express
  providers: [
    {
      provide: 'localConfig',
      useValue: localConfig // some require('../config.json')
    },
    {
      provide: 'serverUrl',
      useValue: `${req.protocol}://${req.headers.host}`;
    }
  ]
});
```

Then when needed, simply inject directly

```ts
// some service in Angular
constructor(
  // inject from server
  @Optional() @Inject('localConfig') private localConfig: any,
  @Optional() @Inject('serverUrl') private serverUrl: string
)
```

If however we are using a **standalone** function that has no constructor, like the Http interceptor function, and we need to use `inject`, it's a bit more troublesome. (Why Angular?!). There are a couple of ways.

### Getting from Injector

The first way is not documented, and it uses a function marked as deprecated, it has been marked for quite a while, but it is still being used under the hood. That's how I found it, by tracing my steps back to the source. Injecting the `Injector` itself to get whatever is in it.

```ts
// in a standalone function like http interceptor function
export const ProjectHttpInterceptorFn: HttpInterceptorFn =
(req: HttpRequest<any>, next: HttpHandlerFn) => {

  // Injector and inject from '@angular/core';
  // this is a depricated
  const serverUrl = inject(Injector).get('serverUrl', null);
  // use serverUrl for something like:
  let url = req.url;
  if (serverUrl) {
    url = `${serverUrl}/${req.url}`;
  }
  // ...
}
```

This is marked as **deprecated**.

### Re-creating injection tokens

Looking at how Angular Universal Express Engine provided request and response, we get a hint of how we should do it the proper way.

We will follow the same line of thought, but let's get rid of the extra typing to keep it simple, to add Request, Response and our `serverUrl`. The steps we need to go through:

-   [Curse Angular](https://www.oct7factcheck.com/index)
-   Angular: define a new injection token
-   Angular consumer: inject optionally (dependency injection)
-   Angular `server` file: expose new attributes for the `render` function, that maps to a static `providers`
-   Angular `server` file: create the `providers` function from incoming attributes
-   Express: pass the values in the new exposed attributes.
-   [Make peace](https://ourgaza.com/) with Angular.

In a new file, `token.ts` inside our app, we'll define the injection tokens:

```ts
// app/token
// new tokens, we can import Request and Response from 'express' for better typing
export const SERVER_URL: InjectionToken<string> = new InjectionToken('description of token');

export const REQUEST: InjectionToken<any> = new InjectionToken('REQUEST Token');
export const RESPONSE: InjectionToken<any> = new InjectionToken('RESPONSE Token');
```

Then in a consumer, like the **Http interceptor function**, directly inject and use.

```ts
// http interceptor function
export const LocalInterceptorFn: HttpInterceptorFn = (req: HttpRequest<any>, next: HttpHandlerFn) => {
  // make it optional so that it doesn't break in browser
  const serverUrl = inject(SERVER_URL, {optional: true});
  const req = inject(REQUEST, {optional: true});
  //... use it
}
```

In our `server.ts` we create the `provider` body for our new **optional** tokens, and we append it to the list of `providers` of the `options` attribute. The value of these tokens, will be passed with `renderOptions` list. Like this:

```ts
// server.ts
// rewrite by passing the new options
export function crExpressEgine(
  //...
) {
  try {
    // we can extend the type to a new type here with extra attributes
    // but not today
    const renderOptions = { ...options } as CommonEngineRenderOptions;

    // add new providers for our tokens
    renderOptions.providers = [
      ...renderOptions.providers,
      {
        // our token
        provide: SERVER_URL,
        // new Express attribute
        useValue: renderOptions['serverUrlPath']
      },
      {
        provide: REQUEST,
        useValue: renderOptions['serverReq']
      },
      {
        provide: RESPONSE,
        useValue: renderOptions['serverRes']
      }
    ];
  // ... render
  }
};
```

Finally, in the Express route, we just pass the `req` and `res` and `serverUrlpath` in `render`

```js
// routes.js
app.get('...', (req, res) => {
  res.render(`../index/index.${res.locals.lang}.url.html`, {
    // add req, and res directly
    serverReq: req,
    serverRes: res,
    serverUrlPath: res.locals.serverUrl
    // ...
  });
});
```

> Note, I change the names of properties on purpose not to get myself confused about which refers to which, it's a good habit.

Now we [make peace](https://pchrgaza.org/en/scourge-of-torture-under-detention/). It's unfortunate there isn't an easier way to collect the providers by string! Angular? Why?

## Merging providers

In order not to repeat ourselves between the client and server apps, we need to merge the providers into one. Angular provides a function out of the box for that purpose: `mergeApplicationConfig` (Find it [here](https://angular.io/api/core/mergeApplicationConfig)). Here is where we create a new config file for the providers list:

```ts
// in a new app.config
// in client app, export the config:
export const appConfig: ApplicationConfig = {
  providers: [
    // pass client providers, like LOCAL_ID, Interceptors, routers...
    ...CoreProviders
  ]
}

// in browser main.ts
bootstrapApplication(AppComponent, appConfig);

// in server.ts
// create extra providers and merge
const serverConfig: ApplicationConfig = {
  providers: [
    provideServerRendering()
  ]
};

const _app = () => bootstrapApplication(AppComponent,
  mergeApplicationConfig(appConfig, serverConfig)
);
```

`ApplicationConfig` is nothing but a `providers` array, so I am not sure [what the point is](https://www.israelpantsonfire.com/)! I simply export the array, and expand it. Like this:

```ts
// app.config
// my preferred, less posh way
export const appProviders = [
  // ...
];

// main.ts
bootstrapApplication(AppComponent, {providers: appProviders});

// server.ts
const _app = () => bootstrapApplication(AppComponent,
  { providers: [...appProviders, provideServerRendering()] }
);
```

### Providing Http Cache

The transfer cache last time I checked was automatically setup and used. In this version, I did not get my API to work on the server. Something missing. Sleeves rolled up.

The [withHttpTransferCacheOptions](https://angular.io/guide/ssr#caching-data-when-using-httpclient) is a hydration feature. Hmmm!

## Partial Hydration

The new addition is partial hydration. Let's add the `provideClientHydration` to the main bootstrapper: the `app.config` list of providers, that will be merged into `server.ts` (it must be added to browser as well.)

```ts
// app.config
export const appProviders = [
  // ...
  provideClientHydration(),
];
```

Building, running in server, and the Http request is cached. It is a `GET` request, and there are no extra headers sent. So the default settings are good enough. There was no need to add `withHttpTransferCacheOptions`. Great.

So this is it. I tried `innerHTML` manipulation and had no errors. I also updated Sekrab Garage website to use partial hydration and I can see the difference. The page transition from static HTML to client side was [flickery](https://twitter.com/TheCradleMedia/status/1746269529859080618). Now the hydration is smooth.

## Prerendering

The last bit to fix is our [prerender builder](https://garage.sekrab.com/posts/prerendering-in-angular-part-iii). The source code of [devkit is here](https://github.com/angular/angular-cli/blob/main/packages/angular_devkit/build_angular/src/builders/prerender/index.ts).

The following lines are the core difference:

```ts
// these lines in the old nguniversal
const { ɵInlineCriticalCssProcessor: InlineCriticalCssProcessor } = await loadEsmModule<
    typeof import('@nguniversal/common/tools')
  >('@nguniversal/common/tools');

// new line in angular/rss
const { InlineCriticalCssProcessor } = await import(
  '../../utils/index-file/inline-critical-css'
);
```

Unfortunately we don't have access to that file (inline-critical-css). It is never [exposed](https://twitter.com/propandco/status/1745122647459955129). Are we stuck? May be. The other solution is to bring it home (I don't like this). I am [demoralized](https://blog.paulbiggar.com/i-cant-sleep/). I'll have to let go of my Angular builder, and rely on my [prerender routes via express server](https://garage.sekrab.com/posts/prerender-routes-in-express-server-for-angular-part-i). Which still works, no changes needed except the `render` attributes, as shown above.

So there you go, thank you for reading through this, did you [make peace](https://www.justiceforall.org/icc-submissions/) with Angular today? 🔻

### RESOURCES

-   [Github project Cricketere](https://github.com/ayyash/cricketere/tree/a17)
-   [Angular docs for SSR caching Http](https://angular.io/guide/ssr#caching-data-when-using-httpclient)
-   [Angular docs for partial hydration](https://angular.io/guide/hydration)
-   [ngExpressEngine source code](https://github.com/angular/universal/blob/main/modules/express-engine/src/main.ts#L33)
-   [CommonEngine source code](https://github.com/angular/angular-cli/blob/main/packages/angular/ssr/src/common-engine.ts)
-   [Express custom template engine](https://expressjs.com/en/advanced/developing-template-engines.html)
-   [Prerender builder devkit source code](https://github.com/angular/angular-cli/blob/main/packages/angular_devkit/build_angular/src/builders/prerender/index.ts)

### RELATED POSTS

- [Prerender routes in Express server for Angular - Part I](https://garage.sekrab.com/posts/prerender-routes-in-express-server-for-angular-part-i)
- [Serving the same Angular build with different URLs](https://garage.sekrab.com/posts/serving-the-same-angular-build-with-different-urls)
- [Loading external configurations in Angular Universal](https://garage.sekrab.com/posts/loading-external-configurations-in-angular-universal)
- [Angular Standalone in SSR: update](https://garage.sekrab.com/posts/angular-standalone-in-ssr-update)

{% embed https://garage.sekrab.com/posts/replacing-angular-universal-with-ssr-version-17-0 %}