Angular 17 SSR - Angular Server Side Rendering in a New Way
Angular 17 SSR - Angular Server Side Rendering in a New Way

In this video you will learn everything that you need to know about Angular server side rendering inside Angular 17.

Adding SSR to your project

Just to remind you previously inside Angular we had a package which was called angular-universal to render Angular first on the server and then on the client. It was not good enough this is why it was completely rewritten and now we are getting another package which is called angular-ssr.

We have 2 possibilities how to add server side rendering inside our project. First possibility is to generate it with option --ssr.

npx -p @angular/cli@17 ng new my-project --ssr

This attribute --ssr will add server side rendering to our project. If we don't put it but just continue with project generation

Questions

As you can see here we got a question if we want our generated project to be installed with server side. With this we will get additional files where entire SSR will be configured for us.

Another possibility if you already have a project is just to install package with configuration

npx -p @angular/cli@17 ng add @angular/ssr

It will install an additional @angular/ssr package and create all files which are needed for server side rendering.

What is changed?

Let's look what we get after installation. The main difference is that inside root we are getting server.ts. Inside we have a normal Express project which will be executed inside Node.

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

  commonEngine
    .render({
      bootstrap,
      documentFilePath: indexHtml,
      url: `${protocol}://${headers.host}${originalUrl}`,
      publicPath: browserDistFolder,
      providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }],
    })
    .then((html) => res.send(html))
    .catch((err) => next(err));
});

This is why most important part of the file. It means that for any route that we request it will render markup from our Angular application. From this function most important property is bootstrap.

import bootstrap from './src/main.server';

As you can see we are getting it from main.server file which was also generated for us. Now we have 2 separate files. main.ts for the client and main.server.ts for the server side.

// src/main.server.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { config } from './app/app.config.server';

const bootstrap = () => bootstrapApplication(AppComponent, config);

export default bootstrap;

This is our main.server.ts. As you can see it is super similar to normal main.ts but it uses special app.config.server and not just normal app.config.

// src/app/app.config.server.ts
import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
import { provideServerRendering } from '@angular/platform-server';
import { appConfig } from './app.config';

const serverConfig: ApplicationConfig = {
  providers: [provideServerRendering()],
};

export const config = mergeApplicationConfig(appConfig, serverConfig);

Here we create a config which merged our normal app.config with serverConfig variable. Inside we just get provideServerRendering which is needed to render our backend.

// src/app/app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';

import { routes } from './app.routes';
import { provideClientHydration } from '@angular/platform-browser';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    provideClientHydration(),
  ],
};

But it is not all. Inside our app.config.ts we got provideClientHydration. It is needed to reuse the markup from the backend and not generate it on client again.

Client hydration

This is why here we must talk about what is client hydration at all. It is something that you need to know about but you can't really control it. The main idea is that we are getting some markup from the backend and previously it was completely deleted by Angular and it recreated the same markup on the client. It didn't use anything from the server.

It doesn't make a lot of sense and this is something which Vue framework did from the start. It was completely possible to initialize Vue application on existing markup. Now Angular does something similar (as well as React) and we can render our application on the server and then Angular reuses exactly the same markup which was already render on the client without need to build it from scratch.

This process of hydration is to transfer all markup and data from backend to client.

And actually we got this process only starting from Angular 16.

Real example

If we start our application like we normally would there is a huge difference. We can open the source of the page and we will see that our application was rendered on server.

Hello app

This is a part of the markup and I can find the text which is rendered on the page. It means that Angular was rendered not only on client but also on server.

It also means that Google can index our page just fine in comparison to typical Javascript application where it is not possible.

What I want to show now is a fully implemented application of users that we rendered inside the table.

Real example

Here we get our list of users from the API. We can also filter our data in the table to show filtered users.

fetchData(): void {
  this.usersService
    .getUsers(this.sorting, this.searchValue)
    .subscribe((users) => {
      this.users = users;
    });
}

This is the most important piece of code in the component. It gets users from the API and saves in local property users.

In ourder for our project to work we must add Http to our configuration.

// src/app/app.config.server.ts
const serverConfig: ApplicationConfig = {
  providers: [provideServerRendering(), provideHttpClient(withFetch())],
};

Here is our server configuration and we didn't only provided here Http module but also put fetch inside which is a recommended approach for rendering applications on server.

Other then this there are zero changes needed and our application is fully working and being rendered on server side.

It is important to remember that Http Angular caches Get requests now. So we won't even see the request in the network.

It means that Angular while rendering server side made an API call, saves these data and passed the to the client.

It might happend that you want to cache not only GET requests from the server but also POST requests. You can do that with additional configuration.

export const appConfig: ApplicationConfig = {
  providers: [
    ...
    provideClientHydration(withHttpTransferCacheOptions({
      includePostRequests: true
    })),
  ],
};

With this code we will cache also our POST requests.

Getting data from the backend

Additionally to that I want to show you how previously we transferred state from the server to the client.

This is how code looked like previously.

ngOnInit(): void {
  if (this.transferState.hasKey(makeStateKey('usersTable'))) {
    this.users = this.transferState.get(makeStateKey('usersTable'), []);
  } else {
    this.fetchData();
  }
}

fetchData(): void {
  this.usersService
    .getUsers(this.sorting, this.searchValue)
    .subscribe((users) => {
      if (isPlatformServer(this.platformId)) {
        this.transferState.set<UserInterface[]>(
          makeStateKey('usersTable'),
          users
        );
      }
      this.users = users;
    });
}

It fetchData method it sets a property in transferState to bring the array of users to the client. In ngOnInit before we do our request we checked if we have a key in transferState to avoid calling API again.

All this code is not needed anymore because Angular transfers state automatically from server to the client and we don't need to do it manually.

AfterNextRender

Now let's look on something that I don't really like. Previously we had 2 helper functions to check if we are on client now or on backend. It allowed us to write code which is only for client for example with condition like this.

export class UsersTableComponent implements OnInit {
  platformId = inject(PLATFORM_ID);

  constructor() {
    if (isPlatformBrowser(this.platformId)) {
      console.log(
        'constructor',
        this.platformId,
        localStorage.getItem('token')
      );
    });
  }
}

This code won't break because isPlatformBrowser checks that the inside is executed only on client. And you can still use isPlatformBrowser and isPlatformServer in order to test where you are.

LS error

Without if condition we get an error on the server.

But now in Angular 17 we got something different. We have an afterNextRender function which will skip the first render which is typically server side rendering. So it is kind of similar to wrapping your code with isPlatformBrowser.

constructor() {
  afterNextRender(() => {
    console.log(
      'constructor',
      this.platformId,
      localStorage.getItem('token')
    );
  });
}

The result is exactly the same.

If we use Angular 17 I think it makes a lot of sense to use new stuff like afterNextRender.

And actually if you want to learn Angular with NgRx from empty folder to a fully functional production application make sure to check my Angular and NgRx - Building Real Project From Scratch course.

📚 Source code of what we've done