Angular Universal & Firebase functions: The missing guide

Enrique oriol
Apr 24, 2018 · 13 min read

Have you ever tried to build Angular Universal in your project for the first time? It wasn’t easy at all, right?

AngularInDepth is moving away from Medium. More recent articles are hosted on the new platform inDepth.dev. Thanks for being part of indepth movement!

Lucky you, I’ve written this simplified guide to configure Angular 5 Universal in your Angular project.

Moreover, I’m gonna give you also a bonus track on how to run Universal in a serverless environment like Firebase Cloud Functions.

Angular 5 Universal guide, step by step

If you haven’t heard about Angular Universal, it is a Server Side Rendering (SSR) tool that allows you to render your Angular app from a server.
Main benefits of SSR are SEO results and reducing initial page load time.

In the past, it was quite complex, but now, Angular CLI v1.6 makes adding Universal to your project easier than ever.

This is gonna be a step-by-step guide, so… let’s start!

1 — Angular CLI 1.6+

First, you’ll need a recent version of Angular CLI (CLI 1.6 or higher).
To install/update your CLI, run from terminal:

npm install -g @angular/cli

2 — Angular project

If you don’t already have a project, create one using the recently installed CLI. Run from terminal:

ng new my-project

Done? Then go to next step.

Otherwise, if you have a project with an older version of CLI, you can update it like this:
2.1 — Open package.json file
2.2 — Modify the version of the “@angular/cli” devDependency to match the most recent one (this one).
2.3 — Remove the node_dependencies folder from your project
2.4 — Install dependencies again running “npm install”.

3 — Creating Universal with the CLI

Go to terminal and run:

ng generate universal

This command should create the following files:

  • src/app/app.server.module.ts
  • src/main.server.ts
  • src/tsconfig.server.json

The first 2 files are like the entry point of your app for server-side execution. The third one is the TS configuration in order to compile your app in a NodeJS environment.

Also, following files should have been updated:

  • package.json: Adds a dependency
  • .angular-cli.json: Configures the CLI in order to compile the Universal app bundle.
  • src/main.ts: Waits to load SSRDOM before rehydrating Angular locally
  • src/app/app.module.ts: Link both Angular versions for rehydrating purposes
  • .gitignore: Ignores new outputs that will be generated during build stage.

4 — Install the new dependencies

The previous command has added `@angular/platform-server` to your dependencies, but it has not been installed.

Install them by running:

npm install

What have you achieved, right now?

Well, at this point you have the basics to build Angular code that can be executed also in the server. Looking carefully .angular-cli.json, you’ll see that a new app has been added to the configuration. It will generate the Universal bundle and move it to dist-server folder.

You can see how it compiles, by running on terminal:

ng build --prod && ng build --prod --app 1 --output-hashing=none

That command should build 2 folders:

  • dist folder: with the original Angular app
  • dist-server folder: With the Universal version of your app, compiled for execution on the server.

However, that’s not enough to get SSR. You’ll need a server that is in charge of rendering the Universal version when it get’s a request. That server can be build using NodeJS, but other technologies can also be used instead.

I’m gonna show you how to create the server using Express, a well know NodeJS framework.

5 — Add additional dependencies

In order to use Express, you’ll need to install some more dependencies:

  • expressExpress framework.
  • @nguniversal/express-engine — Angular Universal Adaptor for Express.
  • @nguniversal/module-map-ngfactory-loader — Dependency for lazy-loading routing from server.
  • reflect-metadata — Dependency required for some polyfills.

You can install them on terminal:

npm install --save @nguniversal/module-map-ngfactory-loader @nguniversal/express-engine express reflect-metadata

6 — Create the Express server for SSR

In project root, create the server folder. Inside, create a new file server/index.ts with this content:

What you see here is a really simple Node server, built with Express framework. Talking about Express would take several posts.

Here I’ll only detail here a few basic points covered in the code:

  1. You’ve created the server by calling the express() function. Then, you set the result in the app variable.
  2. The promise returned from ngExpressEngine is passed as a HTML render engine. That function is a wrapper that helps you by:
    2.1 — Internally call renderModuleFactory: the one that renders the HTML on client requests.
    2.2 — Initial param is AppServerModule, the Angular module you've just created.
    2.3 — You can pass additional providers with data that would only be retrieved by the current running server.
  3. You are using app.get() in order to filter HTTP requests:
    3.1 — In this case, any static file (urls ending with some extension, in glob syntax, the pattern *.*), would be served from dist folder. Webpack would move there the static files required (JS, CSS, imgs) thanks to the changes made by ng generate universalin angular-cli.json file.
    3.2 — The remaining routes would be passed to the server as simple navigation.
  4. Call app.listen to run the server.

7 — Typescript Compiler

You’ve created the Express server by using Typescript, so you’ll need to compile the TS code into plain JS.

On frontend, you are used to creating bundles with minified code, all together in a single file. This approach makes sense for client code because you are reducing the size and number of files to send. However, this approach does not make sense at all for server environments.

NodeJS has a quite efficient modules architecture, and it is loading files locally: Building a bundle with Express dependencies would only serve to increase compilation time.

As building a bundle does not make sense in Node environments, what I suggest you is to only compile index.ts file, in order to convert it to plain JS.

Compiling one file with TS CLI is dead easy…

Check that you have the latest TS CLI, running on terminal:

tsc --version

Otherwise, install it with:

sudo npm install -g typescript

8 — Configuring server TS compiler

Typescript would need to know how do you want to compile the server code. You can detail that by creating a config file:

In server folder, make a tsconfig.ssr.json file with the following content:

This config should compile server/index.ts TS file for Node (that's the reason of using commonjs modules). The result will be placed on the dist-serverfolder.

9 — Adding compiler commands to package.json file

Open package.json file and in the scripts zone, write:

You’ve just created 4 commands that you can run with npm:

  • tsc:server: Compiles the Express server using the previous config file.
  • build:client-and-server-bundles: Compiles your Angular app production ready and using AOT, both browser and server versions.
  • build:universal: Useful command that calls the previous ones. That’s just what you need to prepare the SSR stuff.
  • serve:universal: Runs the Express server you’ve just created.

10 — Testing the results

Everything is ready, now you have to try it!

Let’s start by compiling. Run on terminal:

npm run build:universal

That should compile the client version of your Angular app, an alternate version for servers, and your Express server itself. Now run:

npm run serve:universal

ta-daaaaaaa! If you started from scratch, that’s what you should see:

-But… How do I know if what I see has been rendered on the server instead of the client?

-Nice doubt! Thanks for asking!

If everything’s gone fine, Angular Universal should have rendered the page from Express server, before sending it to your browser. The SSR content should have been loaded in your browser, while the Angular client version is being loaded in background. Once finished, Angular should have linked the client app with the SSR app, in a process called rehydration.

If for any reason the server has not been able to render the page, you are probably watching the same result, except it has been directly generated from the client(no SSR, neither SEO).

Then… how can I check whether Server-Side Rendering is being used or not?

Easy peasy. Right-click on the browser page and select “View page source” option. Other browsers than Chrome have also similar features.

If you are using SSR, your whole website should be shown inside the <app-root> Angular tag. Like in this image:

However, if something went wrong and SSR failed, you should see an empty <app-root> tag. That's the indicator that SSR is not working and that you are watching a client rendered page.

Bonus Track: Deploy on Firebase

For this section, I’m assuming a basic knowledge of Firebase and that you have already created an empty Firebase project. Otherwise, please, go and create a Firebase user and add a new project.

1 — Installing Firebase CLI

Run the command below to install or update Firebase CLI:

npm install -g firebase-tools

2 — Login with Firebase

Run in console:

firebase login

3 — Add Firebase to your project

From Angular project folder, run this command:

firebase init

In the dialogue, mark Functions and Hosting.

Then, pick the Firebase project you want to link the project with.

4 — Firebase functions

You’ll be prompted about using TS or JS. As you have already created the TS server compiler config, you can just pick JS.

Regarding ESLint, you can say “no”, as well as with npm dependencies install.

Tell Firebase that public directory will be dist/browser (we will update that ASAP). Tell Firebase it will be an SPA.

Once the process is finished, you’ll have a functions folder. That's not the structure you want. In fact, the only thing you really need from this process is the package.json file.

Ok, you’re done with the easy part. But now, you need to connect Firebase functions with your Express server and give it access to the files you’ll upload to Firebase hosting.

Let me tell you what, in my opinion, is the optimal way to use together Firebase and Universal.

5 — Firebase functions structure for Universal

Right now your compiled code is distributed between dist and dist-server folders. Moreover, Firebase has created a functions folder, where it expects your cloud functions to live (basically your Express server and its dependencies).

So at this point, your build structure looks like that:

  • dist: Angular (browser) App and static files
  • dist-server: Universal App
  • functions: Express server and dependencies

However, you want the Express server to run from Firebase functions, and it needs to read the Universal App. This structure does not allow it to access that folder.

I suggest you to use this other structure instead:

  • dist: Express Server and dependencies
    browser: Angular (browser) App and static files
    server: Universal App

So…

5.1 — Remove dist and dist-server folders

5.2 - Now, make a new empty dist folder.

6 — package.json for Firebase functions

The package.json file created in functions folder is really important so Firebase can understand cloud functions external dependencies.

Once you deploy Firebase functions, Firebase cloud servers will run npm install so the dependencies defined in package.json will be installed.

6.1 — First step would be, then moving functions/package.json to dist/package.json.

6.2 — Take care! The CLI generated .gitignore file would ignore everything inside dist. It would be nice to add this file to your control version system. You can do so with:

git add dist/package.json -f

6.3 — Remove functions folder

7 — Update firebase.json

Update firebase.json file like this:

7.1 — Let’s start with the easy stuff: the functions property. I've renamed the folder with cloud functions, so that's how I tell Firebase to find the new folder. Firebase will upload the whole dist folder to the cloud functions space on every Firebase cloud functions deploy.

7.2 — Regarding hosting, now files would be stored in dist/browser. But there are a few more details:

Once you visit the page you want the Express server to respond the request. That’s why you use the rewrites property in hosting section. The glob pattern ** means any route, so any route will be handled by thessr function. That's the name you will use to export the firebase function wrapping the Express server.

Important: rewrites rule is only applied when no file exists in Firebase hosting for the matching route.

You need to take care of that. When the root page URL is visited you don’t want to serve the index.html static file that exists in dist/browser. This file is an empty skeleton. Instead of that, you want to call your Express server to render the whole content of the requested page.

To bypass this issue, just rename the index.html file that gets generated by npm run build:universal. This way, Firebase won't find it so it will use ssr function to handle any request to root domain.

8 — Renaming index.html to index-1.html

8.1 — In order to rename the autogenerated file dist/browser/index.html, you must rename the original file, stored in src folder. So please, rename src/index.html to src/index-1.html.

8.2 — There’s one line in your Express server where you define index.htmlas a render entry point. As you have renamed it to index-1.html, you'll need to update also this server/index.ts code fragment:

8.3 — Finally, you should also update .angular-cli.json file so Angular can find index-1.html and move it to dist/browser folder during the build process. You'll see that in a moment.

9 — Update .angular-cli.json in order to use the new structure

9.1 — Open .angular-cli.json and update the index property as well as the outDir of both client and universal apps:

9.2 — Edit server/tsconfig.ssr.json in order to updated also the output directory of the compiled Express server:

10 — Update server to use Firebase

Open again the server/index.ts file to make the following changes:

10.1 — Import firebase:

10.2 — Find Universal app in the right folder (now dist/server):

10.3 — Detect if firebase cloud functions is being used (that would be defined in an environment variable we will define soon):

10.4 — Update DIST_FOLDER, as its value will depend on express being used with Firebase functions or not (running npm run serve:universal for example).

10.5 — Update paths to follow new structure. Now static files are inside dist/browser (see next step).

10.6 — Static files are now served directly by firebase hosting. Your code that serves static content from Express is only required when DISABLE_FIREBASE is true. Same happens with code running the server (app.listen): Now it is Firebase who launches the server.

10.7 — Export Express server as a Firebase function. Remember naming the exported function as ssr.

11 — Update the serve:universal command

As you see, the server is now expecting an environment var DISABLE_FIREBASE (false by default), in order to know if it has to use the Firebase specific code, or the generic Express server approach.

Update the serve:universal command to set this var to true so you can always run the server without Firebase functions.

Remember that your Express server is compiled in dist/index.js.

Update package.json file from root folder:

This way your Universal executions will be faster. Also, using this env var allows you to deploy the same index.ts server in a non-Firebase environment.

12 — Install firebase dependencies in your project

As you’ll see index.ts file is giving you an error.

It cannot find firebase-functions package. You need to install it in the root project (not in dist). Moreover, you'll also need to add firebase-admin dependency. From the root project folder, add both dependencies with:

npm install firebase-functions firebase-admin --save

13 — Check that it’s still working :)

First, without Firebase functions. Simply check that nothing has got broken.

Compile the whole code with:

npm run build:universal

and run it with:

npm run serve:universal

If you’ve followed all the steps, your app should be working fine. Hooray!

Now let’s see if it is also working locally with Firebase. Run:

firebase serve

This would run a local server emulating firebase hosting (usually at http://localhost:5000) and a local endpoint linked to your firebase function.

If you go to the hosting URL, the web page should be rendered as expected. Moreover, you should also see in the terminal that the ssr method has been fired.

Remember to check, also, that the source code your browser has received is the SSR one, instead of an empty template(<app-root></app-root>). It should look like this:

So far so good?

GREAT!

Now, let’s upload it to Firebase!

14 — Add Universal dependencies to Firebase package.json

You have included the Firebase package.json file in dist folder. This file includes the dependencies required by Firebase functions. Of course, you also need to include the Universal project dependencies.

Update dist/package.json dependencies section, so it looks like this:

IMPORTANT: If your dist/package.json file includes the line "main": "lib/index.js",, remove it.

15 — Deploy to Firebase

Uploading hosting and functions stuff to Firebase is only 1 command away. Run:

firebase deploy

That’s all!!!!

Photo by Ray Hennessy on Unsplash

It would take some time, but at the end, you should receive a “success” message, as well as the link to your Firebase served app.

As always, make sure that Firebase is working fine and you are getting the SSR page.

Do you want to access the source code?https://github.com/kaikcreator/universal-firebase-starter

Personal thoughts

Its been a long article, so I am asking myself… have you reached the end?

YES?

Then, let me know!! → I’ll be really happy to know it ;)

Back to my thoughts…

I’m quite a fan of Angular Universal. It allows you to reduce load time as well as provide SEO features to your Angular website. And with tools like Firebase you don’t even need to be a backend expert to use it!!!

And Firebase… ok, maybe you can feel conditioned by their environment and I would not recommend it to build custom solutions for big clients.

However, I think it has a great potential for startups: It drastically reduces costs and development / system maintenance times. It is a must for personal projects that have some backend requirements.

What about you? What do you think?

Do you like the article? Then share it! ;)

Angular In Depth

The place where advanced Angular concepts are explained