Skip to content

pegasusheavy/nestjs-angular-ssr

Repository files navigation

@lexmata/nestjs-angular-ssr

Nest Logo + Angular Logo

Angular SSR (v19+) module for NestJS framework

License: MIT CI npm version npm downloads

Description

A NestJS module that integrates Angular SSR (Server-Side Rendering) for Angular v19+ applications. This module provides a seamless way to serve Angular applications with server-side rendering through a NestJS backend, similar to how @nestjs/ng-universal worked for older Angular Universal versions.

Features

  • 🚀 Angular v19+ Support - Built for the modern Angular SSR API
  • 📦 Easy Integration - Simple forRoot() and forRootAsync() configuration
  • 💾 Built-in Caching - Configurable response caching with pluggable storage
  • 🔌 Request/Response Injection - Access Express request/response in Angular components
  • Performance - Static file serving with caching headers
  • 🛠️ Customizable - Custom error handlers, cache key generators, and more

Installation

pnpm add @lexmata/nestjs-angular-ssr

# or with npm
npm install @lexmata/nestjs-angular-ssr

# or with yarn
yarn add @lexmata/nestjs-angular-ssr

Prerequisites

  • Node.js >= 18.0.0
  • NestJS >= 10.0.0
  • Angular >= 19.0.0 with SSR configured

Usage

Basic Setup

Import AngularSSRModule in your NestJS application module:

import { Module } from '@nestjs/common';
import { join } from 'path';
import { AngularSSRModule } from '@lexmata/nestjs-angular-ssr';

// Import the bootstrap function from your Angular SSR server entry
import bootstrap from './path-to-angular/server/main.server';

@Module({
  imports: [
    AngularSSRModule.forRoot({
      browserDistFolder: join(process.cwd(), 'dist/my-app/browser'),
      bootstrap: () => bootstrap(),
    }),
  ],
})
export class AppModule {}

Async Configuration

For more complex setups where you need to inject dependencies:

import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { AngularSSRModule } from '@lexmata/nestjs-angular-ssr';

@Module({
  imports: [
    ConfigModule.forRoot(),
    AngularSSRModule.forRootAsync({
      imports: [ConfigModule],
      useFactory: async (configService: ConfigService) => ({
        browserDistFolder: configService.get('BROWSER_DIST_FOLDER'),
        bootstrap: async () => {
          const { default: bootstrap } = await import('./angular/server/main.server');
          return bootstrap();
        },
      }),
      inject: [ConfigService],
    }),
  ],
})
export class AppModule {}

Full Example

Here's a complete example showing how to integrate Angular SSR with a NestJS application.

Project Structure

my-app/
├── src/
│   ├── app/                      # NestJS application
│   │   ├── app.module.ts
│   │   ├── app.controller.ts
│   │   └── api/                  # Your API modules
│   │       └── users/
│   │           ├── users.module.ts
│   │           └── users.controller.ts
│   └── main.ts                   # NestJS entry point
├── angular/                      # Angular application
│   ├── src/
│   │   ├── app/
│   │   ├── main.ts
│   │   └── main.server.ts
│   └── angular.json
├── dist/
│   └── angular/
│       ├── browser/              # Angular browser build
│       └── server/
│           └── server.mjs        # Angular SSR server bundle
└── package.json

Angular Server Entry Point

Your Angular application needs to export the AngularAppEngine. With Angular v19+, your angular/server.ts should look like:

// angular/server.ts
import { AngularAppEngine, createRequestHandler } from '@angular/ssr';
import { AppServerModule } from './src/main.server';

const angularApp = new AngularAppEngine();

export default angularApp;

NestJS Main Entry Point

// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app/app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // Global prefix for API routes (optional)
  app.setGlobalPrefix('api', {
    exclude: ['/'], // Exclude root for Angular SSR
  });

  await app.listen(4000);
  console.log('Application is running on: http://localhost:4000');
}

bootstrap();

NestJS App Module

// src/app/app.module.ts
import { Module } from '@nestjs/common';
import { join } from 'path';
import { AngularSSRModule } from '@lexmata/nestjs-angular-ssr';
import { UsersModule } from './api/users/users.module';

// Dynamic import for the Angular SSR engine
const angularBootstrap = async () => {
  const { default: angularApp } = await import('../../dist/angular/server/server.mjs');
  return angularApp;
};

@Module({
  imports: [
    // Your API modules
    UsersModule,

    // Angular SSR module - should be imported LAST
    AngularSSRModule.forRoot({
      browserDistFolder: join(process.cwd(), 'dist/angular/browser'),
      bootstrap: angularBootstrap,
      // Optional: customize render paths
      renderPath: '*',
      // Optional: configure caching
      cache: {
        expiresIn: 60000, // 1 minute
      },
    }),
  ],
})
export class AppModule {}

API Controller Example

// src/app/api/users/users.controller.ts
import { Controller, Get, Param } from '@nestjs/common';

@Controller('users')
export class UsersController {
  @Get()
  findAll() {
    return [
      { id: 1, name: 'John Doe' },
      { id: 2, name: 'Jane Doe' },
    ];
  }

  @Get(':id')
  findOne(@Param('id') id: string) {
    return { id: Number(id), name: 'John Doe' };
  }
}

Complete App Module with All Features

// src/app/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { join } from 'path';
import { AngularSSRModule, InMemoryCacheStorage } from '@lexmata/nestjs-angular-ssr';
import { UsersModule } from './api/users/users.module';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
    }),
    UsersModule,

    // Angular SSR with async configuration
    AngularSSRModule.forRootAsync({
      imports: [ConfigModule],
      useFactory: async (configService: ConfigService) => {
        // Dynamic import of Angular SSR engine
        const { default: angularApp } = await import('../../dist/angular/server/server.mjs');

        return {
          browserDistFolder: join(process.cwd(), 'dist/angular/browser'),
          bootstrap: async () => angularApp,

          // Cache configuration
          cache: {
            expiresIn: configService.get('SSR_CACHE_TTL', 60000),
            storage: new InMemoryCacheStorage(),
          },

          // Custom error handler
          errorHandler: (error, req, res) => {
            console.error('SSR Error:', error.message);
            res.status(500).send(`
              <!DOCTYPE html>
              <html>
                <head><title>Error</title></head>
                <body>
                  <h1>Something went wrong</h1>
                  <p>Please try again later.</p>
                </body>
              </html>
            `);
          },
        };
      },
      inject: [ConfigService],
    }),
  ],
})
export class AppModule {}

Build Scripts (package.json)

{
  "scripts": {
    "build": "npm run build:angular && npm run build:nest",
    "build:angular": "ng build && ng run my-app:server",
    "build:nest": "nest build",
    "start": "node dist/main.js",
    "start:dev": "nest start --watch",
    "start:prod": "node dist/main.js"
  }
}

Environment Configuration

# .env
SSR_CACHE_TTL=60000
NODE_ENV=production

API Reference

forRoot() Options

Property Type Default Description
browserDistFolder string required Path to the Angular browser build directory
bootstrap () => Promise<AngularAppEngine> required Function that returns the Angular SSR engine
serverDistFolder string? - Path to the server bundle directory
indexHtml string? {browserDistFolder}/index.server.html Path to the index.html template
renderPath string | string[]? '*' Route path(s) to render the Angular app
rootStaticPath string | RegExp? '*' Path pattern for serving static files
extraProviders StaticProvider[]? - Additional providers for SSR
inlineCriticalCss boolean? true Inline critical CSS to reduce render-blocking
cache boolean | CacheOptions? true Cache configuration
errorHandler ErrorHandler? - Custom error handler function

Cache Options

Property Type Default Description
expiresIn number? 60000 Cache expiration in milliseconds
storage CacheStorage? InMemoryCacheStorage Custom cache storage implementation
keyGenerator CacheKeyGenerator? UrlCacheKeyGenerator Custom cache key generator

Request and Response Injection

Access Express Request and Response objects in your Angular components using the provided string tokens:

import { Component, Inject, Optional, PLATFORM_ID } from '@angular/core';
import { isPlatformServer } from '@angular/common';
import type { Response } from 'express';
import { RESPONSE } from '@lexmata/nestjs-angular-ssr/tokens';

@Component({
  selector: 'app-not-found',
  template: '<h1>404 - Page Not Found</h1>',
})
export class NotFoundComponent {
  constructor(
    @Inject(PLATFORM_ID) private platformId: object,
    @Optional() @Inject(RESPONSE) private response: Response | null,
  ) {
    if (isPlatformServer(this.platformId) && this.response) {
      this.response.status(404);
    }
  }
}

Note: REQUEST and RESPONSE are string tokens ('ANGULAR_SSR_REQUEST' and 'ANGULAR_SSR_RESPONSE') that work with both NestJS and Angular dependency injection systems.

Custom Cache Storage

Implement the CacheStorage interface for custom caching solutions (e.g., Redis):

import { CacheStorage, CacheEntry } from '@lexmata/nestjs-angular-ssr';

export class RedisCacheStorage implements CacheStorage {
  constructor(private redis: RedisClient) {}

  async get(key: string): Promise<CacheEntry | undefined> {
    const data = await this.redis.get(key);
    return data ? JSON.parse(data) : undefined;
  }

  async set(key: string, entry: CacheEntry): Promise<void> {
    await this.redis.set(key, JSON.stringify(entry));
  }

  async delete(key: string): Promise<boolean> {
    return (await this.redis.del(key)) > 0;
  }

  async clear(): Promise<void> {
    await this.redis.flushdb();
  }

  async has(key: string): Promise<boolean> {
    return (await this.redis.exists(key)) > 0;
  }
}

Custom Cache Key Generator

Create custom cache keys based on request properties:

import { Request } from 'express';
import { CacheKeyGenerator } from '@lexmata/nestjs-angular-ssr';

export class MobileAwareCacheKeyGenerator implements CacheKeyGenerator {
  generateCacheKey(request: Request): string {
    const userAgent = request.headers['user-agent'] || '';
    const isMobile = /mobile/i.test(userAgent) ? 'mobile' : 'desktop';
    const url = request.hostname + request.originalUrl;
    return `${url}:${isMobile}`.toLowerCase();
  }
}

AngularSSRService API

The AngularSSRService can be injected to programmatically manage SSR:

import { Injectable } from '@nestjs/common';
import { AngularSSRService } from '@lexmata/nestjs-angular-ssr';

@Injectable()
export class CacheManagementService {
  constructor(private readonly ssrService: AngularSSRService) {}

  async clearAllCache(): Promise<void> {
    await this.ssrService.clearCache();
  }

  async invalidatePage(cacheKey: string): Promise<boolean> {
    return this.ssrService.invalidateCache(cacheKey);
  }
}

Angular Project Setup

Ensure your Angular project is configured for SSR. With Angular v19+:

ng add @angular/ssr

Your Angular project should have a server entry point that exports the Angular app engine bootstrap function.

Project Structure

nestjs-angular-ssr/
├── lib/
│   ├── index.ts                          # Public API re-exports
│   ├── tokens.ts                         # REQUEST / RESPONSE DI tokens
│   ├── angular-ssr.module.ts             # NestJS dynamic module (forRoot / forRootAsync)
│   ├── angular-ssr.service.ts            # SSR rendering + cache management
│   ├── angular-ssr.middleware.ts         # Express middleware for SSR + static files
│   ├── interfaces/
│   │   ├── angular-ssr-module-options.interface.ts
│   │   ├── cache-key-generator.interface.ts
│   │   └── cache-storage.interface.ts
│   └── cache/
│       ├── in-memory-cache-storage.ts    # Default cache backend
│       └── url-cache-key-generator.ts    # Default cache key strategy
├── tokens/                               # Secondary entry point for @lexmata/nestjs-angular-ssr/tokens
├── example/                              # Full working NestJS + Angular SSR example app
├── docs/                                 # GitHub Pages API docs
├── .github/
│   ├── CONTRIBUTING.md                   # Contributor guide
│   ├── SECURITY.md                       # Security policy
│   ├── PULL_REQUEST_TEMPLATE.md
│   ├── ISSUE_TEMPLATE/                   # Bug report + feature request templates
│   └── workflows/
│       ├── ci.yml                        # Lint, typecheck, test (Node 18/20/22), build
│       └── release.yml                   # Publish to npm + GitHub Packages on release
├── package.json
├── tsconfig.json
├── eslint.config.mjs
├── vitest.config.ts
└── commitlint.config.mjs

Testing

pnpm test               # Run all tests once
pnpm test:watch          # Watch mode
pnpm test:coverage       # Coverage report (V8)

Tests are co-located with source files as *.spec.ts in lib/.

Publishing

Publishing is automated via GitHub Actions. When a GitHub release is created:

  1. The release.yml workflow runs tests and builds the package
  2. Publishes to npm (@lexmata/nestjs-angular-ssr) using NPM_TOKEN
  3. Publishes to GitHub Packages using GITHUB_TOKEN

To publish manually:

pnpm run clean && pnpm run build
pnpm publish --access public

Related Repos

Repo Relationship
lexmata-app-frontend Consumer -- uses this module for Angular SSR in the main app
lexmata-marketing Consumer -- uses this module for the marketing site SSR
lexmata-admin-frontend Consumer -- uses this module for the admin panel SSR

Contributing

We welcome contributions! Please see our Contributing Guide for details.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feat/amazing-feature)
  3. Commit your changes (git commit -m 'feat: add amazing feature')
  4. Push to the branch (git push origin feat/amazing-feature)
  5. Open a Pull Request

Support

License

MIT License - Copyright (c) 2025 Lexmata LLC

See LICENSE for details.

About

No description, website, or topics provided.

Resources

License

Contributing

Security policy

Stars

Watchers

Forks

Releases

No releases published

Sponsor this project

Packages

 
 
 

Contributors