Workers bindings, a working hostname allow-list and in-process API calls for Angular SSR apps on Cloudflare Workers, in ng serve and in production.
| API | Recipes | Troubleshooting | Changelog | Demo |
Note
This is an early 0.x release. Function names may still change before 1.0.0.
Cloudflare's Angular starter gets an app onto Workers, but four things break once it is real:
| Problem | Without ngx-cloudflare | With it |
|---|---|---|
| Hostnames | The generated server.ts allows only localhost, so Angular answers 400 Header "host" ... is not allowed on your *.workers.dev address and custom domains. |
localhost, 127.0.0.1, *.workers.dev and your wrangler routes are allowed. Anything else gets a 400 that says where to add it. |
| Bindings | env, ctx and request.cf never reach Angular code, and ng serve has no bindings at all. |
injectEnv() and friends, on Workers and in ng serve, sharing local state with wrangler dev. |
| Own-API calls | During rendering, HttpClient.get('/api/...') leaves the Worker and fetches its own hostname. |
Answered inside the Worker, and Angular's transfer cache still hands the result to the browser. |
| Log noise | Angular logs a warning for every x-forwarded-* header Cloudflare adds, on every request. |
Those headers are removed before Angular sees them. |
ng add ngx-cloudflareRun it on an app with server-side rendering (ng new --ssr, or after ng add @angular/ssr). It also works on projects made by npm create cloudflare and wrangler setup. It:
- points the build at Workers (
outputMode: server, the neutral SSR platform,wranglerkept out of the bundle), - replaces a stock
src/server.ts(Angular's Express server or Cloudflare's untouched template, keeping any hosts you added to it) and never touches one you customised, - copies the hostnames in your wrangler routes into
ALLOWED_HOSTSinwrangler.jsonc, again each time you run it, - registers
provideCloudflare()in the server config, - creates
wrangler.jsonc, or repairs one that deploys the app as a static site, - switches the stock prerender-everything route to per-request rendering,
- adds
preview,deployandcf-typegenscripts and removes the brokenserve:ssrone.
| Angular | Platform key it sets | Notes |
|---|---|---|
| 22 | ssr.platform |
|
| 21 | ssr.experimentalPlatform |
|
| 20 | ssr.experimentalPlatform |
|
| 19 | ssr.experimentalPlatform |
Apps created without server routing are converted: ng add adds app.routes.server.ts and registers it. |
| 18 and older | Not supported. AngularAppEngine and REQUEST_CONTEXT, which this builds on, arrived in 19. |
None of them needs the nodejs_compat flag.
src/server.ts after ng add:
import { createCloudflareRequestHandler } from 'ngx-cloudflare/server';
export const reqHandler = createCloudflareRequestHandler<Env>({
allowedHosts: ['shop.example.com'],
api: (request, env, ctx) => {
if (new URL(request.url).pathname === '/api/hello') {
return Response.json({ hello: env.GREETING });
}
return null;
},
});
export default { fetch: reqHandler } satisfies ExportedHandler<Env>;Custom domains can also live next to your routes in wrangler.jsonc, so adding one needs no code change:
api takes a function or anything with a fetch method, such as a Hono app. It receives requests under /api/. Return null to let Angular handle the path.
In components, services, resolvers and guards:
import { injectEnv, injectExecutionContext, injectRequestCf } from 'ngx-cloudflare';
export class Visits {
private readonly env = injectEnv<Env>(); // null in the browser
constructor() {
const ctx = injectExecutionContext();
if (this.env && ctx) {
ctx.waitUntil(this.env.VISITS.put('last', new Date().toISOString()));
}
}
}Each function returns null outside a server request: in the browser, while prerendering and at build time. Pass { required: true } to throw a clear error instead.
Run npm run cf-typegen after editing wrangler.jsonc to regenerate the Env type.
ng serve # bindings from wrangler's local platform proxy
npm run preview # production build in the local Workers runtime
npm run deploy # production build, then wrangler deployng serve and wrangler dev share local state in .wrangler/, so data you add in one shows up in the other.
This repository is an Angular workspace. The root app is a guestbook that runs on the library: entries in D1, a visit counter in KV, and a form posting to /api/entries. It doubles as the end-to-end test target.
npm ci
npm run build:lib
npm run db:migrate # create the local D1 table
npm start # or: npm run previewnpm run test:lib # library: server core, inject functions, HTTP backend, ng add
scripts/test-lib-angular.sh 19 # the library's Angular tests against Angular 19 (or 20, 21, 22)
npx ng test ngx-cloudflare-demo # demo
scripts/e2e-ng-add.sh 22 <package.tgz> [kind] # create an app, ng add, then check wrangler dev and ng servekind is ng-new (default), cloudflare for a project made by npm create cloudflare, or wrangler-setup for one configured by wrangler setup.
CI runs the library tests on Angular 19, 20, 21 and 22, the end-to-end script on a fresh app for each of those versions, and on the Cloudflare starter and a wrangler setup project.
These ship with the package too, for cases the core flow does not cover:
ngx-cloudflare/testing: fake bindings for TestBed.isInProcessRequest(request): lets anapihandler tell a render-time call from a browser call.cachePages: true: keeps pages with a sharedCache-Controlmax-age in the Workers cache. See Caching pages.dispatchfalls back to theASSETSbinding, so a page can read its own static JSON files in-process.- Options to turn parts off:
apiPrefix: false,stripProxyHeaders: false,devPlatform: false,provideCloudflare({ inProcessRequests: false }). - A Durable Objects recipe, including a layout that works in
ng serve.
- Durable Objects and anything else that imports
cloudflare:workerscannot live insrc/server.ts, because Angular runs that file in Node. See the Durable Objects recipe. - Routes rendered at build time (
RenderMode.Prerender) have no bindings. - Cloudflare Pages and Analog are not supported. Analog reaches Workers through Nitro's own preset.
| Next | Why |
|---|---|
| Workers beside an existing Node server | A separate build configuration, so adopting this does not replace a working deployment. |
| Bindings during prerendering | Build-time rendering has no request to carry them. |
| Typed helpers for queues and cron | scheduled and queue exports already work next to fetch; helpers would add types. |
MIT licensed, see LICENSE.
Cloudflare and Cloudflare Workers are trademarks of Cloudflare, Inc. Angular is a trademark of Google LLC. This project is not affiliated with, endorsed by, or sponsored by either.