skip to content

In @angular/ssr, how do AngularNodeAppEngine and AngularAppEngine differ, and what does handle() return for a URL Angular does not own?

level: middleimportance: should knowfreq 30%

answer

  1. Node request vs Web Request
  2. an adapter over the other
  3. null means not mine
  4. requestContext becomes a token

basics

~10 s

AngularAppEngine takes a Web Request and returns a Response; AngularNodeAppEngine accepts Node requests too and converts them. Both resolve to null for a non-Angular URL so the server can call the next handler.

solid answer

~30 s

Both classes turn a request into a rendered or served response according to the matched route's `RenderMode`. `AngularAppEngine` (from `@angular/ssr`) accepts a Web `Request` and resolves to a `Response`, so it fits any Fetch-API runtime. `AngularNodeAppEngine` (from `@angular/ssr/node`) also accepts Node's `IncomingMessage` or `Http2ServerRequest`, converts it and delegates to an `AngularAppEngine`; it also reads `NG_ALLOWED_HOSTS` and `NG_TRUST_PROXY_HEADERS` from the environment, and you write the result with `writeResponseToNodeResponse`. `handle()` resolves to `null` when the URL is not an Angular route, and the generated `server.ts` then calls `next()`. Its optional second argument becomes the `REQUEST_CONTEXT` token. An unrecognised host gets a 400.

code

ts · 20 lines
ts
import {
  AngularNodeAppEngine,
  createNodeRequestHandler,
  writeResponseToNodeResponse,
} from '@angular/ssr/node';
import express from 'express';

const app = express();
const angularApp = new AngularNodeAppEngine();

app.use((req, res, next) => {
  angularApp
    .handle(req, { tenant: res.locals['tenant'] })
    .then((response) =>
      response ? writeResponseToNodeResponse(response, res) : next(),
    )
    .catch(next);
});

export const reqHandler = createNodeRequestHandler(app);

go deeper

for a junior

Recall that the server file creates an app engine and calls handle() for each request, and that a null result means the request is not Angular's.

for a middle

Explain which engine accepts which request type, what writeResponseToNodeResponse does, and how requestContext reaches components as REQUEST_CONTEXT.

for a senior

Wire the engine into an existing server without breaking other routes, configure the host allow-list, and keep the exported reqHandler working for the CLI.

for a principal

Decide which engine fits the runtime the organisation deploys to, and keep server-computed data flowing through REQUEST_CONTEXT rather than ad-hoc header parsing.

## Two engines, one job `@angular/ssr` ships an **app engine**: the object that takes an incoming HTTP request, decides which Angular route and `RenderMode` it matches, and produces an HTTP response. There are two public classes. | | `AngularAppEngine` | `AngularNodeAppEngine` | | --- | --- | --- | | Import from | `@angular/ssr` | `@angular/ssr/node` | | `handle()` accepts | A Web `Request` | Node `IncomingMessage`, `Http2ServerRequest`, or a Web `Request` | | `handle()` returns | `Promise<Response or null>` | `Promise<Response or null>` | | Writing the result | Return the `Response` from your fetch handler | `writeResponseToNodeResponse(response, res)` | | Handler wrapper | `createRequestHandler` | `createNodeRequestHandler` | | Options from env vars | No | `NG_ALLOWED_HOSTS`, `NG_TRUST_PROXY_HEADERS` | `AngularNodeAppEngine` is a thin adapter: it converts a Node request into a Web `Request` and delegates to an `AngularAppEngine`. It also registers `unhandledRejection` and `uncaughtException` logging on the process once when zone.js is not loaded. `AngularAppEngine` works on any runtime that speaks the Fetch API `Request` and `Response` types. The older `CommonEngine` class is still exported from `@angular/ssr/node` but is marked **deprecated** in favour of these two. ## What handle() does For each request the engine: 1. **Validates the host.** The request's host is checked against `allowedHosts` (constructor option, and for the Node engine also the `NG_ALLOWED_HOSTS` variable). An unrecognised host gets a **400 Bad Request**, a guard against server-side request forgery. Proxy headers such as `X-Forwarded-Host` are ignored unless `trustProxyHeaders` allows them. 2. **Matches the URL** against the extracted route tree built from your `ServerRoute` list. 3. **Acts on the render mode:** - `Prerender`: returns the stored HTML with an `ETag`, or a `304 Not Modified` when `If-None-Match` matches; - `Server`: renders the app for this request and returns the resulting HTML; - `Client`: returns the client-side shell HTML. 4. **Returns `null`** when the URL is not an Angular route at all. ## Why null matters `null` means "not mine". The generated `server.ts` relies on it: ```ts app.use((req, res, next) => { angularApp .handle(req) .then((response) => response ? writeResponseToNodeResponse(response, res) : next(), ) .catch(next); }); ``` A `null` passes control to the next middleware, so API routes, custom 404 handling or other apps can share the server. A few well-known non-Angular URLs such as `/favicon.ico` return `null` immediately. Treating `null` as an error, or writing an empty 200 for it, breaks that chain. ## Passing server data into the app `handle(request, requestContext)` takes an optional second argument. Whatever you pass becomes the value of the **`REQUEST_CONTEXT`** injection token during a `RenderMode.Server` render, next to `REQUEST` (the Web request) and `RESPONSE_INIT` (the mutable status and headers). That is the clean channel for data your server already computed, such as a tenant id resolved by middleware, instead of re-parsing headers inside components. ## The exported reqHandler Both templates end by exporting a handler: - `export const reqHandler = createNodeRequestHandler(app);` for Node; - `createRequestHandler(handler)` for a fetch-style handler. The Angular CLI's dev server and build use this export to run your server code during development and prerendering, so it should stay exported even when you start the server yourself. ## Choosing - Pick **`AngularNodeAppEngine`** when the app runs on a Node HTTP server or a Node-based framework; it saves you the request conversion and reads the host allow-list from the environment. - Pick **`AngularAppEngine`** when the runtime hands you Web `Request` objects and expects a `Response` back. The rendering itself, the route matching and the render modes are identical; only the edges of the request and response differ. ## Mistakes to avoid - Turning a `null` result into a 404 or a 500 inside the Angular middleware, which hides every route registered after it. - Registering the Angular middleware before API routes or static files, so it sees requests it will only decline. - Setting `allowedHosts` to `'*'`: the engine logs a warning because that disables its own host check and leaves validation to a proxy. - Re-parsing request headers inside components for data the server already knows, instead of passing it through `REQUEST_CONTEXT`.

  • Why must server.ts keep exporting reqHandler?
    The Angular CLI's dev server and build import that export to run your server code during development and prerendering. `createNodeRequestHandler` or `createRequestHandler` marks the function for that purpose, so removing the export breaks those flows even if production starts the server itself.
  • What does the engine do with a request whose Host header is not allowed?
    It answers 400 Bad Request without rendering. The host is checked against `allowedHosts` (and `NG_ALLOWED_HOSTS` for the Node engine) to prevent server-side request forgery, and forwarded host headers are ignored unless `trustProxyHeaders` permits them.

saying these in an interview costs you the question

  • AngularNodeAppEngine renders differently from AngularAppEngine
  • A null from handle() means rendering failed and should become a 500
  • AngularAppEngine can only run inside a Node process
  • CommonEngine is still the recommended engine for new SSR apps
  • Data for components must be smuggled through custom request headers