Angular SSR with vanilla Node.js
Tags: node, angular, tutorial, typescript
Published: 2020-06-11 23:50:12

##Introduction

Hello! Last time we was talking about [Angular Universal boilerplate setup](https://dev.to/igorfilippov3/angular-universal-boilerplate-setup-239e). And today, we will also talk about angular universal tuning, but without already baked libraries like **express** and **ngExpressEngine**. Only vanilla Node.js, only hardcore :)

I suppose this article will be useful for developers who want to have a deeper understanding how to setup angular application at server side or to connect angular with web servers which is not represented in official [repo](https://github.com/angular/universal/tree/master/modules)

##Let's go!

I assume that your already have **@angular/cli** installed.

We will start from scratch. First create a new project:

```
ng new ng-universal-vanilla
cd ng-universal-vanilla
```

Then run the following CLI command

```
ng add @nguniversal/express-engine
```

Actually, we do not need express web server. But we need a plenty of another files produced by this command.

First of all, take a look at *server.ts* file. At the line 18, you can find **ngExpressEngine**. This is the heart of angular server side rendering. It is express-based template engine, which use angular universal [CommonEngine](https://github.com/angular/universal/blob/master/modules/common/engine/src/engine.ts) under the hood. And **CommonEngine** it is exactly, what we need.

In the root directory create *ssr/render-engine/index.ts* file, with few lines of code in it:
```javascript
import { ɵCommonEngine as CommonEngine, ɵRenderOptions as RenderOptions } from "@nguniversal/common/engine";
import { readFileSync } from "fs";

const templateCache = {};

export function renderEngine() {
  const engine: CommonEngine = new CommonEngine();

  return async function (filepath: string, renderOptions: RenderOptions) {
    try {
      if (templateCache[filepath]) {
        renderOptions.document = templateCache[filepath];
      } else {
        renderOptions.document = readFileSync(filepath).toString();
        templateCache[filepath] = renderOptions.document;
      }

      return await engine.render(renderOptions);
      
    } catch (err) {
      throw new Error(err);
    }
  }
}
```

A renderEngine function creates an instance of CommonEngine and returns another function which mission is to run angular bundle in **server/main.js** and produce an html template. In addition, we use a *templateCache* to store the *index.html* source code for better performance.
With this approach, we able not to run the synchronous **readFile** function any time when server receives a request from the browser. Now, go to the *server.ts* file, remove everything from it and add following lines:

```javascript
import "zone.js/dist/zone-node";
import { createServer, IncomingMessage, ServerResponse, Server } from "http";
import { AppServerModule } from "./src/main.server";
import { APP_BASE_HREF } from "@angular/common";
import { join } from "path";
import { renderEngine } from "./ssr/render-engine";

const browserFolder: string = join(process.cwd(), "dist/ng-universal-vanilla/browser");
const indexTemplate: string = join(browserFolder, "index.html");
const port = process.env.PORT || 4000;

const renderTemplate = renderEngine();

const app: Server = createServer(async (req: IncomingMessage, res: ServerResponse) => {
  
  const html = await renderTemplate(indexTemplate, {
    url: `http://${req.headers.host}${req.url}`,
    bootstrap: AppServerModule,
    providers: [
      { provide: APP_BASE_HREF, useValue: "/" },
    ]
  });

  res.writeHead(200);
  res.end(html);
});

app.listen(port, () => console.log(`Server is listening at ${port} port`));
```

The code is almost same as it was before deleting. But instead of express web server we added our **renderEngine** which we wrote earlier and some stuff from [http](https://nodejs.org/dist/latest-v6.x/docs/api/http.html) Node.js module to create a web server. Now, run the following commands and open your browser at http://localhost:4000
```
npm run build:ssr
npm run serve:ssr
```

If you did everything right you should see an Angular welcome page. We did it! We generated an angular template and sent it to the browser. But, to say the truth it is not enough for full server operation. If you open a developer tools console, you will see this message:
![Static files not served](https://dev-to-uploads.s3.amazonaws.com/i/o8cp7lqhleixjlsk3d8g.png)
This happens because we are sending html, but not serving our static files which lay in the *index.html* file. We have to update our *server.ts* file a little bit:
```javascript
..other imports
import { readFile } from "fs";

const browserFolder: string = join(process.cwd(), "dist/ng-universal-vanilla/browser");
const indexTemplate: string = join(browserFolder, "index.html");
const port = process.env.PORT || 4000;

const renderTemplate = renderEngine();

const app: Server = createServer((req: IncomingMessage, res: ServerResponse) => {

  const filePath: string = browserFolder + req.url;

  readFile(filePath, async (error, file) => {
    if (error) {
      const html = await renderTemplate(indexTemplate, {
        url: `http://${req.headers.host}${req.url}`,
        bootstrap: AppServerModule,
        providers: [
          { provide: APP_BASE_HREF, useValue: "/" },
        ]
      });
      res.writeHead(200);
      res.end(html);
    } else {
      if (req.url.includes(".js")) {
        res.setHeader("Content-Type", "application/javascript")
      }

      if (req.url.includes(".css")) {
        res.setHeader("Content-Type", "text/css");
      }

      res.writeHead(200);
      res.end(file);
    }
  });

});

app.listen(port, () => console.log(`Server is listening at ${port} port`));
```

We imported a **readFile** function from node.js built-in module [fs](https://nodejs.org/dist/latest-v6.x/docs/api/fs.html). On each request we try to read a file in the *dist/ng-universal-vanilla/browser* folder. If it exists, we send it to the browser.

Content-type header is also important, without it browser will not know in what manner handle our .css or .js file. If file is not exist, **readFile** function throws an error and we know that this url should be rendered by angular universal engine. Of course, at first look, handling of angular templates with error condition looks weird, but even node.js official [docs](https://nodejs.org/dist/latest-v6.x/docs/api/fs.html#fs_fs_access_path_mode_callback) recommend this approach instead of checking with **fs.acess** function.

*HINT: In real application, your static files will be served with something like Nginx or Apache. This approach, is only for demonstration of angular universal engine with vanilla node.js server*

Now, run the following commands and reload the page.
```
npm run build:ssr
npm run serve:ssr
```

Our angular application is ready to go!

## Handling cookies and DI provider

In next few lines, I want to show how to deal with cookies with vanilla node.js server and how to provide a request object to angular application.

First of all, we need to create an injection token for request object, which can be used later in a DI provider.
Create *ssr/tokens/index.ts* file and add a following code
```javascript
import { InjectionToken } from "@angular/core";
import { IncomingMessage } from "http";

export declare const REQUEST: InjectionToken<IncomingMessage>;
```

Then, provide it in the **renderTemplate** function in *server.ts* file
```javascript
...
import { REQUEST } from "./ssr/tokens";
...
const html = await renderTemplate(indexTemplate, {
  url: `http://${req.headers.host}${req.url}`,
  bootstrap: AppServerModule,
  providers: [
    { provide: APP_BASE_HREF, useValue: "/" },
    { provide: REQUEST, useValue: req },
  ]
});
...
```

That's almost all. We prepared our request injection token, and now can use it.
Open *app.server.module.ts* and update it like this
```javascript
import { NgModule, Inject, Injectable, Optional } from '@angular/core';
import { ServerModule } from '@angular/platform-server';

import { AppModule } from './app.module';
import { AppComponent } from './app.component';
import { REQUEST } from "../../ssr/tokens";
import { IncomingMessage } from "http";

@Injectable()
export class IncomingServerRequest {
  constructor(@Inject(REQUEST) private request: IncomingMessage) { }

  getHeaders() {
    console.log(this.request.headers, "headers");
  }
}

@NgModule({
  imports: [
    AppModule,
    ServerModule,
  ],
  bootstrap: [AppComponent],
  providers: [
    { provide: "INCOMING_REQUEST", useClass: IncomingServerRequest },
  ]
})
export class AppServerModule {
  constructor(@Optional() @Inject("INCOMING_REQUEST") private request: IncomingServerRequest) {
    this.request.getHeaders();
  }
}
```

Here, we created and provided a standalone class *IncomingServerRequest* which have our request object injected and it is ready to use.

Again, build and run our app
```
npm run build:ssr
npm run serve:ssr
```

In the console of our web server you should see a list of headers related to a request from your browser.

###What about cookies?

First we have to extend a request object annotations. So, in the *ssr/models/index.ts* file add this code:
```javascript
import { IncomingMessage } from "http";

export interface IncomingMessageWithCookies extends IncomingMessage {
  cookies: {[key: string]: string};
}
```

Now, we can add a new property to our request object without conflicts in typescript. To parse cookies, install a **cookie** package from npm.
```
npm i --save cookie
```

then update a *server.ts* file a little bit
```javascript
...
import { parse } from "cookie";

...

const app: Server = createServer((req: IncomingMessageWithCookies, res: ServerResponse) => {

  const filePath: string = browserFolder + req.url;

  readFile(filePath, async (error, file) => {
    if (error) {    

      req.cookies = parse(req.headers.cookie);

      const html = await renderTemplate(indexTemplate, {
        url: `http://${req.headers.host}${req.url}`,
        bootstrap: AppServerModule,
        providers: [
          { provide: APP_BASE_HREF, useValue: "/" },
          { provide: REQUEST, useValue: req },
        ]
      });
      res.writeHead(200);
      res.end(html);
    } else {
      if (req.url.includes(".js")) {
        res.setHeader("Content-Type", "application/javascript")
      }

      if (req.url.includes(".css")) {
        res.setHeader("Content-Type", "text/css");
      }

      res.writeHead(200);
      res.end(file);
    }
  });
});
```

and a *app.server.module.ts*
```javascript
...
import { IncomingMessageWithCookies } from "../../ssr/models";

@Injectable()
export class IncomingServerRequest {
  constructor(@Inject(REQUEST) private request: IncomingMessageWithCookies) { }

  getHeaders() {
    console.log(this.request.headers, "headers");
  }

  getCookies() {
    console.log(this.request.cookies)
  }
}

@NgModule({
  imports: [
    AppModule,
    ServerModule,
  ],
  bootstrap: [AppComponent],
  providers: [
    { provide: "INCOMING_REQUEST", useClass: IncomingServerRequest },
  ]
})
export class AppServerModule {
  constructor(@Optional() @Inject("INCOMING_REQUEST") private request: IncomingServerRequest) {
    this.request.getHeaders();
    this.request.getCookies();
  }
}
```

Also, do not forget to update a *ssr/tokens/index.ts* file

```javascript
import { InjectionToken } from "@angular/core";
import { IncomingMessageWithCookies } from "../models";

export declare const REQUEST: InjectionToken<IncomingMessageWithCookies>;
```

And that's it! Now we have an angular application with server side rendering setup, without express and other frameworks.

I hope this article was useful for you. 

P.S. Source code can be found at [github](https://github.com/IgorFilippov3/ng-universal-vanilla) .