| title | Angular CLI and Express |
|---|---|
| description | Mount the devtools hub in the Express server of an Angular SSR app. |
server.ts, load the overlay in main.ts, and open the panel from a button on your page.
In an *Angular app with server-side rendering, the devtools run inside your Express server. You add a middleware on the server and load the overlay in the browser.
This setup mounts the devtools in the Expressserver.ts that Angular SSR generates. For an Analog app, follow Vite and Analog instead. For a server on Hono, h3 or Fastify, follow Hono, h3 and Fastify.
Add @pangular-inspector/devtools as a dependency, because server.ts imports it. See Installation.
Add initPangularHub() to server.ts, before your other routes.
Import the overlay in main.ts, in development only.
Start the app in development mode and click the Pangular Inspector button in the corner of the page.
// src/server.ts
import express from 'express';
import {initPangularHub} from '@pangular-inspector/devtools/hub';
const app = express();
const devtools = initPangularHub({ws: false});
app.use(devtools.nodeMiddleware);The full-page viewer is at http://localhost:4000/__devframes/. The hub is built on @devframes/hub, so other devframe tools can join the same dock.
Mount the middleware before express.static and the Angular SSR handler, so the devtools routes answer first.
// src/server.ts
const app = express();
const devtools = initPangularHub({ws: false});
app.use(devtools.nodeMiddleware); // devtools first
app.use(express.static(browserDistFolder, {index: false}));
app.use((req, res, next) => {
angularApp
.handle(req)
.then((response) => (response ? writeResponseToNodeResponse(response, res) : next()))
.catch(next);
});The middleware only handles requests under its base path (/__devframes/ by default). Everything else goes to the next handler.
The browser talks to the hub over server-sent events or a WebSocket. Pick one with the ws option:
// src/server.ts
const devtools = initPangularHub({ws: false});// src/server.ts
const devtools = initPangularHub({ws: {sidecar: true}});With ws: false there is no WebSocket, and the browser connects over SSE on the same port. It is the simplest choice: every request goes through your Express server, including under ng serve.
With ws: {sidecar: true}, the WebSocket runs on its own port, picked automatically.
ng serve runs server.ts again after a rebuild that changes the server output. The process keeps one hub per base, so each new initPangularHub() call closes the hub from the run before, along with its side-car port. The MCP token stays the same: the hub keeps it in a file. See Send a token.
initPangularHub() accepts the options of initHub() from @devframes/hub, apart from devframes and ui. These are the ones you are most likely to set:
| Option | Default | What it does |
|---|---|---|
base |
'/__devframes/' |
Where the hub is mounted. The devtools panel lives at <base>pangular/. |
ws |
false uses server-sent events only. { sidecar: true } runs the WebSocket on its own port. |
|
enabled |
on unless NODE_ENV is production |
false mounts nothing: the middlewares only call next(). true mounts the hub in production too, with a warning. See Production. |
auth |
on | false turns off the one-time code. {tokens: [...]} keeps it on and trusts clients that send one of the pairing tokens. |
allowedOrigins |
loopback origins and the Chrome extension | Extra origins allowed to reach the hub, such as a tunnel. false turns the origin check and the request gate off. |
allowLan |
false |
Answer other machines, such as a phone on Wi-Fi. Needs the one-time code on. |
terminals |
false |
Add a dock that runs the project's build and test commands. See Terminals. |
mcp |
a bearer token | Mounts the MCP endpoint at <base>__mcp and asks for a bearer token. With auth: false the default is 'auto': it mounts once agent tools exist and asks for no token. See Send a token. |
The hub also takes the devtools options, such as inspectors, agent, actions, redaction and limits. See Configuration. Pass browserDistFolder, the folder that holds index.csr.html, to let the Force Client render override serve it. See SSR overrides.
The hub answers only requests from this machine, sent to a loopback Host such as localhost:4000, the same gate as the Vite plugin. Other requests to the hub get 403. Pass allowLan: true to also answer other machines, such as a Capacitor app on Wi-Fi.
The hub protects its connection with a one-time code by default. The server prints the code, and a browser can read data only after it exchanges that code. On a machine only you use, pass auth: false to turn the code off.
The origin check is on by default too. Only loopback origins and the Chrome extension, chrome-extension://dcogniffeelebaolkkfbopmjcblhblfk, can open the WebSocket. A list in allowedOrigins keeps both and adds its entries. A custom origin registry passed as allowedOrigins still accepts the published extension. allowedOrigins: false turns the check off and prints a warning once. A build of the extension with another ID needs its own entry, with the ID from chrome://extensions:
// src/server.ts
import {initPangularHub} from '@pangular-inspector/devtools/hub';
const devtools = initPangularHub({
allowedOrigins: ['https://tunnel.example', 'chrome-extension://<id>'],
});Access and redaction covers both checks.
The demo app in this repository mounts the hub like this:
// src/server.ts
const auth = process.env['PANGULAR_AUTH'] === 'true';
const devtools = initPangularHub({
ws: {sidecar: true},
auth,
});
if (devtools.enabled) {
app.use(devtools.nodeMiddleware);
app.use(devtools.ssrMiddleware);
}It turns the one-time code off unless PANGULAR_AUTH is true. Don't copy that setting. Keep the one-time code on for your own apps. The demo keeps the default origin check.
The hub is off when NODE_ENV is production. initPangularHub() then mounts nothing: nodeMiddleware and ssrMiddleware only call next(), and devtools.enabled is false. Check enabled to skip the app.use() calls, as the demo does.
Pass enabled: true to mount the hub in production anyway, for example on a staging server only you can reach. The hub prints a warning when it starts. Pass enabled: false to keep it off in any environment.
NODE_ENV when initPangularHub() runs. If your production server does not set NODE_ENV=production, the hub stays on. Set it, or pass enabled yourself.
The overlay collects live data from the page. Import it after bootstrap, in development only:
// src/main.ts
import {bootstrapApplication} from '@angular/platform-browser';
import {App} from './app/app';
import {appConfig} from './app/app.config';
bootstrapApplication(App, appConfig)
.then(() => {
if (typeof ngDevMode === 'undefined' || ngDevMode) {
return import('@pangular-inspector/devtools/overlay');
}
return undefined;
})
.catch((err) => console.error(err));ngDevMode is false in production builds, so the import never runs there and the overlay stays out of your production bundle.
A floating button appears on your page. It opens the devtools with one dock entry per tool:
| Dock entry | Shows |
|---|---|
| Angular | Dashboard, components, routes, signals, injectors, forms, pipes, and SSR & HTTP |
| NgRx | Store patterns from source, and live state and actions |
| Analog | File routes, server calls, render modes and lint (a notice in non-Analog apps) |
| Angular Native | Components, signals, injectors and store of a connected Angular Native app |
| NativeScript | Setup steps for NativeScript apps |
| Capacitor | Plugin calls of a connected Capacitor app, or setup steps when none is connected |
Popup and hub covers the panel, its dock modes and deep links.
When you run ng serve, the Angular dev server runs server.ts too, so the hub answers on port 4200 as well.
ng serve
# open http://localhost:4200 and click the Pangular Inspector buttonng build --configuration development
node dist/<your-app>/server/server.mjs
# open http://localhost:4000 and click the Pangular Inspector buttonTo test the real Express process, build with the development configuration and start server.mjs. Replace <your-app> with your project name.
ng build uses the production configuration by default. ngDevMode is false there, so the overlay is never imported and the button never appears. The live tabs need --configuration development. In this repository, the same applies to pnpm build.
To fill the SSR & HTTP tab, add the interceptor and hydration hooks to your app config:
// src/app/app.config.ts
import {provideHttpClient, withFetch} from '@angular/common/http';
import {ApplicationConfig} from '@angular/core';
import {provideClientHydration} from '@angular/platform-browser';
import {providePangularHttp, withPangular} from '@pangular-inspector/devtools/http';
export const appConfig: ApplicationConfig = {
providers: [
provideClientHydration(),
provideHttpClient(withFetch(), withPangular()),
providePangularHttp(),
],
};withPangular() records requests and applies fault rules. providePangularHttp() captures hydration warnings before the overlay loads. In production builds the interceptor passes requests through untouched.
Register withPangular() before your own interceptors, for example provideHttpClient(withPangular(), withInterceptors([authInterceptor])). It then records requests as the app makes them, and fault rules apply before anything else.
To fill the SSR requests table, add the hub's ssrMiddleware after the hub middleware and before the Angular handler:
// src/server.ts
const devtools = initPangularHub({ws: false});
app.use(devtools.nodeMiddleware);
app.use(devtools.ssrMiddleware);It is optional. It also adds a Server-Timing header to each traced response. If the server uses compression(), mount ssrMiddleware after it. See SSR & HTTP.
SSR and the devtools middleware must run in the same Express process. Otherwise the server-side calls never reach the tab.
The SSR & HTTP guide covers interceptor order and fault injection in detail.
To mount only the devtools panel without the dock, use initDevframe(). The devframe entry point re-exports it from the Devframe copy the package depends on:
// src/server.ts
import pangular, {initDevframe} from '@pangular-inspector/devtools/devframe';
const devtools = initDevframe(pangular, {base: '/__pangular/'});
app.use(devtools.nodeMiddleware);The overlay looks for /__pangular/ too. Without the hub, every tab sits in one tab bar.
ng serve, or build with --configuration development. Then check that main.ts imports the overlay.
The page could not reach the hub. Check that the hub middleware is mounted before express.static and the SSR handler, and that base matches the path the overlay uses. See No devtools server found.
Check that the server is running and that the hub middleware is mounted before express.static and the SSR handler. The panel keeps trying to connect; click Retry now to try at once.
The request did not come from this machine, was sent to a host other than a loopback host, or came from an origin the hub does not trust. The terminal names a refused origin or host. Open the app on localhost, add the origin to allowedOrigins, or pass allowLan: true for another device.
With auth on, a browser reads data only after it exchanges the one-time code the server printed. On a machine only you use, pass auth: false.
Add withPangular() and providePangularHttp(), and run SSR in the same Express process as the hub. Prerendered routes make no requests at runtime.
What the overlay sends, and how to point it at a custom mount path.
Dock modes, the hub rail, deep links and connection status.
Interceptor order, fault rules and hydration warnings.
Who can reach the hub, and what is redacted.