Angular Server Side Rendering | Angular SSR | Angular Universal
Angular Server Side Rendering | Angular SSR | Angular Universal

In this post you will learn what is server side rendering inside Angular and how you can implement it.

Actually from the start I want to say that in comparison to React not a lot of people are rendering Angular on server side. But actually it works just fine.

What is it and how to use it?

So the first question here is what is server side rendering at all?

Server side means that your application will be rendered on the backend first.

If we check normal application you will see the source code of the initial page. It happens because it was rendered on the server.

Source code

But if we check the source code of typical client framework like Angular we won't have anything inside body.

Source code client

We just have here a single element where we render the whole application. In this case Javascript built the whole markup on the client. So our page is fully empty when it is rendered.

It has pros and cons and the main problem is that our page is not indexed by search engines like for example Google. Which actually means if you write some articles then they won't be indexed inside your application. Additionally to that user won't see anything on your page until the whole Javascript will be parsed and rendered on the page.

But from my personal perspective using server side rendering is really a rare case. If we are talking about SPA typically we don't build a website with posts that we want to index. We build an app where user without page reload can do everything.

But enough talking let's convert our existing Angular application to server side rendered application.

Installing packages

The first step here is to install additional package.

ng add @nguniversal/express-engine

This command won't just add a package but also generate additional files that are needed for server side rendering.

But the main problem for me was that Angular installed wrong package version and nothing worked afterwards. This is why I reinstalled this package with the specific version like my Angular packages.

ng add @nguniversal/express-engine@14.2.3

In this case it installed everything correctly.

After installation we have quite a lot of changes. Inside our package.json we can find this package and new commands to start our application server side.

new commands

Also inside our source folder we have not only main.ts but also main.server.ts.

// src/main.server.ts
import '@angular/platform-server/init';

import { enableProdMode } from '@angular/core';

import { environment } from './environments/environment';

if (environment.production) {
  enableProdMode();
}

export { AppServerModule } from './app/app.server.module';
export { renderModule } from '@angular/platform-server';

As you can see it is kind of similar to main.ts but it loads ApssServerModule instead. Which actually means that on the backend we don't use AppModule but this module.

Now let's look on our new app.server.module.ts.

@NgModule({
  imports: [
    AppModule,
    ServerModule,
  ],
  bootstrap: [AppComponent],
})
export class AppServerModule {}

What you can see here is an import of AppModule which means the whole application is still loaded like before but we import ServerModule additionally here for all backend rendering logic.

The last file which was generated (which is the most important file) is server.ts.

export function app(): express.Express {
  const server = express();
  const distFolder = join(process.cwd(), 'dist/appName/browser');
  const indexHtml = existsSync(join(distFolder, 'index.original.html')) ? 'index.original.html' : 'index';

  server.engine('html', ngExpressEngine({
    bootstrap: AppServerModule,
  }));

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

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

  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;

  const server = app();
  server.listen(port, () => {
    console.log(`Node Express server listening on http://localhost:${port}`);
  });
}
export * from './src/main.server';

This is an express server which will do all heavy load to render our Angular application on the backend. As you can see quite a lot of stuff was generated. At the end of the file we import main.server file and on the top we start our express server.

The most important part in the whole is ngExpressEngine call. The main point is that inside it our server will analyse our code and modules and create the markup from our Angular application.

Now let's start our application not in the client only mode but with server side. In order to do that we can use dev:ssr command.

npm run dev:ssr

As you can see our application works out of the box but it is different. Here inside our source code the whole application is rendered on backend first. But after the initial server side rendering the whole application continue to work like a client application.

Initial project

What is interesting we fetched the list of users and rendered them inside component. On the backend it made for us a call to the API and rendered all users on the server. Which is really wise as I would expect that it renders the empty view on the backend and won't wait for the API data.

Checking platform

Our application works but in the console we directly get an error that localStorage is not defined.

LS error

Why it happens at all? Local storage doesn't exist on the server which means that we can't use it (on anything related to the browser) on the backend.

How we can fix that? We need to add check and execute client code inside.

export class CurrentUserService {
  platformId: Object;
  constructor(@Inject(PLATFORM_ID) platformId: Object) {
    this.platformId = platformId;
  }

  setCurrentUser() {
    if (isPlatformBrowser(this.platformId) && localStorage.getItem('token')) {
      this.currentUser$.next({ id: '1', name: 'Foo' });
    } else {
      this.currentUser$.next(null);
    }
  }
}

Here in setCurrentUser our code was broken because I used localStorage.getItem. But now I injected here PLATFORM_IDand added isPlatformBrowser check which returns true on the client.

As you can see we don't get an error anymore.

How to reuse a state?

One more thing that I want to show you is not known by a lot of people. We can reuse state from the server side and pass it to the client. Which actually means we can save the list of our users on the backend and avoid making an API call on the client because we already have this data. For this we must use TransferState

export class UsersTableComponent implements OnInit {
  ...
  platformId: Object;

  constructor(
    ...
    private transferState: TransferState,
    @Inject(PLATFORM_ID) platformId: Object
  ) {
    this.platformId = platformId;
  }

  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;
      });
  }
}

Here we injected PLATFORM_ID and TransferState inside our component which renders a list of users. Now inside fetchData we set our fetched users if we are on the server side. To set data to the transferState we use a set method. As a first argument we pass there not a string but makeStateKey('usersTable') which generates unique identifier.

Now inside ngOnInit we wrap our code in hasKey to check if we have data inside our transferState. If yes then we will set them to this.users without making an API call.

As you can see in browser we eliminated duplicate fetch on the client because we passed this data from the backend.

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