Have you ever tried to build Angular Universal in your project for the first time? It wasn’t easy at all, right?
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:
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:
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:
- express — Express 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:
- You’ve created the server by calling the
express()function. Then, you set the result in the
- The promise returned from
ngExpressEngineis 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.
- 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
distfolder. 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.
app.listento 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:
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:
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
9 — Adding compiler commands to package.json file
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:
3 — Add Firebase to your project
From Angular project folder, run this command:
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
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-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
5.1 — Remove
5.2 - Now, make a new empty
6 — package.json for Firebase functions
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 installso the dependencies defined in
package.jsonwill be installed.
6.1 — First step would be, then moving
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
7 — Update firebase.json
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 the
ssr function. That's the name you will use to export the firebase function wrapping the Express server.
rewritesrule 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
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
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
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
11 — Update the
As you see, the server is now expecting an environment var
false by default), in order to know if it has to use the Firebase specific code, or the generic Express server approach.
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
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:
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?
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.
dist/package.json dependencies section, so it looks like this:
IMPORTANT: If your
dist/package.jsonfile 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:
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
Its been a long article, so I am asking myself… have you reached the end?
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! ;)