Skip to main content

Angular Server Integration

Introduction

Angular Server Integration is a critical aspect of implementing Server-Side Rendering (SSR) in Angular applications. While traditional Angular apps run entirely in the browser (client-side), SSR allows your application to render on the server first and then hydrate in the browser. This approach offers significant benefits including improved performance, better SEO, and enhanced user experience for initial page loads.

In this guide, you'll learn how to integrate an Angular application with a server environment, specifically using Express.js, which is the default server option in Angular Universal. You'll understand the architecture, configuration, and implementation details required to run Angular applications on the server.

Understanding Angular Universal

Angular Universal is the official package for implementing server-side rendering in Angular applications. It works by pre-rendering your Angular application on the server and sending the fully rendered HTML to the client, where it's then "hydrated" with JavaScript to become a fully interactive application.

Key Benefits of Server Integration

  1. Improved SEO - Search engines can crawl your fully rendered content
  2. Faster Initial Page Load - Users see content more quickly
  3. Better Performance on Mobile Devices - Less JavaScript processing on device
  4. Enhanced Social Media Sharing - Proper preview of content when shared

Setting Up Angular Universal

Let's start by adding Angular Universal to an existing Angular project.

Installing Universal

First, you need to add Universal to your project using Angular CLI:

bash
ng add @nguniversal/express-engine

This command will:

  • Install necessary dependencies
  • Create server-side application module (app.server.module.ts)
  • Create a server file (server.ts) with Express.js
  • Update Angular configuration files
  • Add build scripts to package.json

Understanding the Generated Files

After running the command, several new files are created:

  1. server.ts - The Express server that will serve your Angular app
  2. app.server.module.ts - The server version of your app module
  3. tsconfig.server.json - TypeScript configuration for the server
  4. main.server.ts - The entry point for the server application

Let's look at the server.ts file more closely:

typescript
import 'zone.js/node';

import { APP_BASE_HREF } from '@angular/common';
import { ngExpressEngine } from '@nguniversal/express-engine';
import * as express from 'express';
import { existsSync } from 'fs';
import { join } from 'path';

import { AppServerModule } from './src/main.server';

// The Express app is exported so that it can be used by serverless Functions.
export function app(): express.Express {
const server = express();
const distFolder = join(process.cwd(), 'dist/your-app-name/browser');
const indexHtml = existsSync(join(distFolder, 'index.original.html')) ? 'index.original.html' : 'index';

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

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

// Example Express Rest API endpoints
// server.get('/api/**', (req, res) => { });
// Serve static files from /browser
server.get('*.*', express.static(distFolder, {
maxAge: '1y'
}));

// All regular routes use the Universal engine
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;

// Start up the Node server
const server = app();
server.listen(port, () => {
console.log(`Node Express server listening on http://localhost:${port}`);
});
}

// Webpack will replace 'require' with '__webpack_require__'
// '__non_webpack_require__' is a proxy to Node 'require'
// The below code is to ensure that the server is run only when not requiring the bundle.
declare const __non_webpack_require__: NodeRequire;
const mainModule = __non_webpack_require__.main;
const moduleFilename = mainModule && mainModule.filename || '';
if (moduleFilename === __filename || moduleFilename.includes('iisnode')) {
run();
}

export * from './src/main.server';

This server file uses Express.js and the ngExpressEngine provided by Angular Universal to render your application on the server.

Building and Running the Universal App

To build and run your server-side application, Angular added two new scripts to your package.json:

bash
# Build the application for production
npm run build:ssr

# Serve the application with Universal rendering
npm run serve:ssr

When you run these commands:

  1. The regular browser application is built
  2. The server application is built
  3. The Express server serves both versions appropriately

Customizing Server Integration

Now that we have the basic setup, let's explore how to customize the server integration for common use cases.

Adding Server-side APIs

One of the most common customizations is adding API endpoints to your Express server. This can be done directly in your server.ts file:

typescript
// In server.ts

// Add API endpoint
server.get('/api/data', (req, res) => {
res.json({
message: 'This is data from the server',
timestamp: new Date()
});
});

Handling Environment-specific Code

When working with Universal, your code runs in two different environments: browser and server. Sometimes you need to write environment-specific code:

typescript
import { isPlatformBrowser, isPlatformServer } from '@angular/common';
import { Component, Inject, PLATFORM_ID } from '@angular/core';

@Component({
selector: 'app-example',
template: `<div>Platform: {{ platformName }}</div>`
})
export class ExampleComponent {
platformName: string;

constructor(@Inject(PLATFORM_ID) private platformId: Object) {
if (isPlatformBrowser(platformId)) {
this.platformName = 'Browser';
// Browser-only code: Access window, document, localStorage
} else if (isPlatformServer(platformId)) {
this.platformName = 'Server';
// Server-only code
}
}
}

Transferring Data from Server to Client

A powerful feature of Angular Universal is the ability to transfer data from the server to the client using TransferState. This prevents the need to make the same HTTP requests twice (on server and then again in the browser):

First, import the necessary modules:

typescript
// In app.module.ts
import { BrowserModule, BrowserTransferStateModule } from '@angular/platform-browser';

@NgModule({
imports: [
BrowserModule.withServerTransition({ appId: 'my-app' }),
BrowserTransferStateModule,
// other modules
],
// ...
})
export class AppModule { }

// In app.server.module.ts
import { ServerModule, ServerTransferStateModule } from '@angular/platform-server';

@NgModule({
imports: [
AppModule,
ServerModule,
ServerTransferStateModule,
// other modules
],
// ...
})
export class AppServerModule { }

Then, use it in a service:

typescript
import { Injectable, PLATFORM_ID, Inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { TransferState, makeStateKey } from '@angular/platform-browser';
import { isPlatformServer } from '@angular/common';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';

const DATA_KEY = makeStateKey<any>('my-data');

@Injectable({
providedIn: 'root'
})
export class DataService {
constructor(
private http: HttpClient,
private transferState: TransferState,
@Inject(PLATFORM_ID) private platformId: Object
) {}

getData(): Observable<any> {
// Check if we have data in transfer state
if (this.transferState.hasKey(DATA_KEY)) {
const data = this.transferState.get(DATA_KEY, null);
// Remove it to avoid using it again
this.transferState.remove(DATA_KEY);
return of(data);
} else {
// Get data from API
return this.http.get('/api/data').pipe(
tap(data => {
if (isPlatformServer(this.platformId)) {
// If we're on the server, store data to transfer to client
this.transferState.set(DATA_KEY, data);
}
})
);
}
}
}

Real-world Application: E-commerce Product Page

Let's build a practical example of a product page that benefits from server-side rendering:

Product Service

typescript
// product.service.ts
import { Injectable, PLATFORM_ID, Inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { TransferState, makeStateKey } from '@angular/platform-browser';
import { isPlatformServer } from '@angular/common';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';

export interface Product {
id: number;
name: string;
description: string;
price: number;
imageUrl: string;
}

const PRODUCT_KEY = makeStateKey<Product>('product');

@Injectable({
providedIn: 'root'
})
export class ProductService {
constructor(
private http: HttpClient,
private transferState: TransferState,
@Inject(PLATFORM_ID) private platformId: Object
) {}

getProduct(id: number): Observable<Product> {
// Create a specific key for this product ID
const productKey = makeStateKey<Product>(`product-${id}`);

if (this.transferState.hasKey(productKey)) {
const product = this.transferState.get(productKey, null);
this.transferState.remove(productKey);
return of(product);
} else {
return this.http.get<Product>(`/api/products/${id}`).pipe(
tap(product => {
if (isPlatformServer(this.platformId)) {
this.transferState.set(productKey, product);
}
})
);
}
}
}

Product Component

typescript
// product.component.ts
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, ParamMap } from '@angular/router';
import { Observable, switchMap } from 'rxjs';
import { Product, ProductService } from './product.service';
import { Title, Meta } from '@angular/platform-browser';

@Component({
selector: 'app-product',
template: `
<div class="product-container" *ngIf="product$ | async as product">
<h1>{{product.name}}</h1>
<img [src]="product.imageUrl" [alt]="product.name">
<div class="price">${{product.price}}</div>
<p class="description">{{product.description}}</p>
<button class="add-to-cart">Add to Cart</button>
</div>
`,
styles: [`
.product-container { max-width: 800px; margin: 0 auto; padding: 20px; }
img { max-width: 100%; }
.price { font-size: 24px; color: #e91e63; margin: 16px 0; }
.add-to-cart { background: #e91e63; color: white; border: none; padding: 10px 20px; }
`]
})
export class ProductComponent implements OnInit {
product$: Observable<Product>;

constructor(
private route: ActivatedRoute,
private productService: ProductService,
private title: Title,
private meta: Meta
) { }

ngOnInit() {
this.product$ = this.route.paramMap.pipe(
switchMap((params: ParamMap) => {
const id = +params.get('id');
return this.productService.getProduct(id);
})
);

// Set SEO metadata when product loads
this.product$.subscribe(product => {
this.title.setTitle(`${product.name} - My Store`);
this.meta.updateTag({ name: 'description', content: product.description.substring(0, 160) });
this.meta.updateTag({ property: 'og:title', content: product.name });
this.meta.updateTag({ property: 'og:description', content: product.description.substring(0, 160) });
this.meta.updateTag({ property: 'og:image', content: product.imageUrl });
});
}
}

Server API Endpoint

Now, let's add a product API endpoint to our server:

typescript
// In server.ts

// Sample product data
const products = [
{
id: 1,
name: 'Smartphone X',
description: 'Latest smartphone with incredible camera and long battery life.',
price: 999.99,
imageUrl: '/assets/smartphone.jpg'
},
{
id: 2,
name: 'Laptop Pro',
description: 'Powerful laptop for professionals, with high performance and sleek design.',
price: 1299.99,
imageUrl: '/assets/laptop.jpg'
}
];

// Product API endpoint
server.get('/api/products/:id', (req, res) => {
const id = +req.params.id;
const product = products.find(p => p.id === id);

if (product) {
// Add artificial delay to demonstrate TransferState benefit
setTimeout(() => {
res.json(product);
}, 500);
} else {
res.status(404).json({ message: 'Product not found' });
}
});

This example demonstrates:

  1. Server-side rendering a product page for SEO benefits
  2. Using TransferState to avoid duplicating HTTP requests
  3. Setting SEO metadata dynamically based on product data
  4. Creating a simple API endpoint in the Express server

Performance Optimizations for Server Integration

Here are additional optimizations to enhance your server integration:

Prerendering Static Routes

For routes that don't change frequently, consider prerendering them at build time:

bash
ng run your-app-name:prerender

You can configure which routes to prerender in your angular.json file:

json
"prerender": {
"builder": "@nguniversal/builders:prerender",
"options": {
"routes": [
"/",
"/about",
"/contact",
"/products/1",
"/products/2"
]
}
}

Cache Control

Add proper cache control headers for static assets and optionally for rendered pages:

typescript
// In server.ts
// For static assets
server.get('*.*', express.static(distFolder, {
maxAge: '1y',
setHeaders: (res, path) => {
if (path.includes('index.html')) {
// Don't cache index.html
res.setHeader('Cache-Control', 'public, max-age=0');
}
}
}));

// For rendered pages with short cache time
server.get('*', (req, res) => {
// Cache rendered pages for 10 minutes
res.setHeader('Cache-Control', 'public, max-age=600');
res.render(indexHtml, { req, providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }] });
});

Summary

In this guide, we covered the essentials of Angular Server Integration for Server-Side Rendering:

  1. Adding Angular Universal to an existing Angular project
  2. Understanding the server setup with Express.js
  3. Building and running the Universal application
  4. Customizing the server for API endpoints and other features
  5. Handling environment-specific code
  6. Transferring data from server to client with TransferState
  7. Building a real-world example of an e-commerce product page
  8. Optimizing performance with prerendering and caching

By implementing server-side rendering with Angular Universal, you can significantly improve your application's performance, SEO, and user experience. The initial page load will be faster, search engines can better index your content, and users will see meaningful content more quickly.

Additional Resources

Exercises

  1. Create a blog application with SSR that displays posts from a mock API
  2. Implement TransferState for a complex data structure like a product catalog
  3. Add server-side caching to your Express server for better performance
  4. Create custom middleware for your Express server to handle authentication
  5. Implement lazy loading modules with Angular Universal

Remember that server-side rendering is particularly beneficial for content-heavy applications or those where SEO and social sharing are important. For highly interactive single-page applications with authenticated users, client-side rendering might be sufficient.



If you spot any mistakes on this website, please let me know at feedback@compilenrun.com. I’d greatly appreciate your feedback! :)