Skip to content

About

Workers bindings, a working hostname allow-list and in-process API calls for Angular SSR on Cloudflare Workers, in ng serve and in production.

Topics

Resources

Stars

6 stars

Watchers

0 watching

Forks

Latest commit

 

History

59 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ngx-cloudflare

Maintained: yes Angular 19 to 22 Cloudflare Workers CI status License: MIT Sponsor

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.

Why

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.

Install

ng add ngx-cloudflare

Run 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, wrangler kept 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_HOSTS in wrangler.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, deploy and cf-typegen scripts and removes the broken serve:ssr one.
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.

Usage

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:

"routes": [{ "pattern": "shop.example.com", "custom_domain": true }],
"vars": { "ALLOWED_HOSTS": "shop.example.com" }

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.

Develop and deploy

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 deploy

ng serve and wrangler dev share local state in .wrangler/, so data you add in one shows up in the other.

The demo

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 preview

Tests

npm 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 serve

kind 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.

Beyond the basics

These ship with the package too, for cases the core flow does not cover:

  • ngx-cloudflare/testing: fake bindings for TestBed.
  • isInProcessRequest(request): lets an api handler tell a render-time call from a browser call.
  • cachePages: true: keeps pages with a shared Cache-Control max-age in the Workers cache. See Caching pages.
  • dispatch falls back to the ASSETS binding, 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.

Limits

  • Durable Objects and anything else that imports cloudflare:workers cannot live in src/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.

Roadmap

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.

Trademarks and licence

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.

About

Workers bindings, a working hostname allow-list and in-process API calls for Angular SSR on Cloudflare Workers, in ng serve and in production.

Topics

Resources

Stars

6 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages