| 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 @santoshyadavdev/ng-devtools and devframe. See Installation.
Add initNgDevtoolsHub() 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 amber button in the corner of the page.
// src/server.ts
import express from 'express';
import {initNgDevtoolsHub} from '@santoshyadavdev/ng-devtools/hub';
const app = express();
const devtools = initNgDevtoolsHub({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 = initNgDevtoolsHub({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 = initNgDevtoolsHub({ws: false});// src/server.ts
const devtools = initNgDevtoolsHub({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 initNgDevtoolsHub() call closes the hub from the run before, along with its side-car port. A generated MCP token stays the same until the process exits.
initNgDevtoolsHub() 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>ng-devtools/. |
ws |
false uses server-sent events only. { sidecar: true } runs the WebSocket on its own port. |
|
auth |
on | false turns off the one-time code. |
allowedOrigins |
loopback origins and the Chrome extension | Extra origins allowed to open the WebSocket. A list replaces the Chrome extension default. false turns the origin check off. |
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.
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 gate off.
The origin check is on by default too. Only loopback origins and the Chrome extension can open the WebSocket. If you pass your own allowedOrigins list, it keeps loopback origins but drops the extension. Add chrome-extension://<id> to the list, with the ID from chrome://extensions:
// src/server.ts
import {initNgDevtoolsHub} from '@santoshyadavdev/ng-devtools/hub';
const devtools = initNgDevtoolsHub({
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['NG_DEVTOOLS_AUTH'] === 'true';
const devtools = initNgDevtoolsHub({
ws: {sidecar: true},
auth,
});
app.use(devtools.nodeMiddleware);It turns the one-time code off unless NG_DEVTOOLS_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.
initNgDevtoolsHub() has no production switch of its own. If your server.ts also runs in production, decide there whether to mount it.
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('@santoshyadavdev/ng-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) |
| NativeScript | A Coming Soon placeholder |
| Capacitor | A Coming Soon placeholder |
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 amber buttonng build --configuration development
node dist/<your-app>/server/server.mjs
# open http://localhost:4000 and click the amber 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 {provideNgDevtoolsHttp, withNgDevtools} from '@santoshyadavdev/ng-devtools/http';
export const appConfig: ApplicationConfig = {
providers: [
provideClientHydration(),
provideHttpClient(withFetch(), withNgDevtools()),
provideNgDevtoolsHttp(),
],
};withNgDevtools() records requests and applies fault rules. provideNgDevtoolsHttp() captures hydration warnings before the overlay loads. In production builds the interceptor passes requests through untouched.
Register withNgDevtools() before your own interceptors, for example provideHttpClient(withNgDevtools(), withInterceptors([authInterceptor])). It then records requests as the app makes them, and fault rules apply before anything else.
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() from devframe/initiate:
// src/server.ts
import {initDevframe} from 'devframe/initiate';
import ngDevtools from '@santoshyadavdev/ng-devtools/devframe';
const devtools = initDevframe(ngDevtools, {base: '/__ng-devtools/'});
app.use(devtools.nodeMiddleware);The overlay looks for /__ng-devtools/ 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. Then reload the page.
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 withNgDevtools() and provideNgDevtoolsHttp(), 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.