How to host React, Angular, Vue and other single-page applications on Cloudflare Workers with static assets

Cloudflare Workers introduced support for static assets in September 2024 as part of the effort to unify Cloudflare Workers and Pages. That means that it’s now possible to host your frontend single-page applications on Workers. And it’s my favorite way to host SPAs so it’s worth showing you how to set it up.

spa-workers-assets.png

TLDR: I created this repository with sample React, Vue, and Angular projects to showcase how these can be hosted on Workers with static assets.

Read on if you want to know why you might prefer using Workers static assets over Pages, and why each setup step is necessary.

Cloudflare Workers static assets vs. Cloudflare Pages

I prefer Workers to host my single-page applications because: 1) Workers are better integrated with the rest of the Cloudflare developer platform (see rate limiting, durable objects, observability, and other differences between Pages and Workers on the compatibility matrix). 2) Workers also give you full access to specify how to handle each API or HTTP request, which is more flexible than Pages’ Functions.

Pages does have some features that can be convenient for certain workflows, such as deploy hooks and branch deploy controls. It’s worth reviewing the compatibility matrix to see the tradeoffs.

How to get your single-page application hosted on Workers static assets

Hosting your single-page application on Workers varies if you want to have custom handling of routes & APIs in addition to your single-page application, or if you’re just hosting a single-page application/static website.

If you want to have Workers + single-page application hosted on Workers Static Assets:

  1. Create a Cloudflare Workers project: npm create cloudflare@latest. Select Hello World example.

  2. Create your single-page application within this Worker project cd <WORKERS PROJECT NAME>, npm create vite@latest .

  3. Configure your wrangler.toml/wrangler.json file to specify your single-page application’s dist or build folder. You also must specify the binding keyword of ASSETS which will be used in the next step.

    //if using wrangler.toml
    assets = { binding = "ASSETS", directory = "./spa-app/dist" }
    
    //if using wrangler.json
    "assets": {
        "binding": "ASSETS", // only required when there is Workers code to configure navigation fallback
        "directory": "./spa-app/dist",
    }
    
  4. At the end of your Workers code, you need to serve your static assets. This is necessary in order to handle navigation fallback (if a user directly visits a page that is not a static asset, nor handled by the API, your single-page application assets must be served since your 404 or custom pages need to be handled by the single-page application).

    export default {
      async fetch(request, env) {
        const url = new URL(request.url);
        if (url.pathname.startsWith("/api/")) {
          // TODO: Add your custom /api/* logic here.
          return new Response("Ok");
        }
    
        // Passes the incoming request through to the assets binding.
        // No asset matched this request, so this will evaluate `not_found_handling` behavior.
        return env.ASSETS.fetch(request);
      },
    };
    
  5. Change your dev, start and deploy scripts in your Workers project’s package.json. These should first perform a build of the child single-page application project before running the Workers commands. In the below example, spa-app contains our React/Angular/Vue/SPA app and calls npm run build (or otherwise configured) to build the SPA project.

    {
    "name": "spa-on-workers-assets",
    "scripts": {
        "deploy": "cd spa-app && npm run build && cd .. && wrangler deploy",
        "dev": "cd spa-app && npm run build && cd .. && wrangler dev",
        "start": "cd spa-app && npm run build && cd .. && wrangler dev",
        ...
    },
    ...
    }
    

Pro-tip: When developing locally, you may find yourself wanting some level of hot reloading. The above scripts don’t include this. Instead, you can open a separate terminal instance, navigate to the single-page application project, and run npx vite build --watch in order to build your single-page application project on every save. You’ll then be able to refresh your Workers application in the browser and it will be serving the new version of your application.

If you only want to host your single-page application on Workers Static Assets (without APIs or custom Worker code):

  1. Create a Cloudflare Workers project with static assets (only): npm create cloudflare@latest -- --experimental. Select Hello World example and Hello World - Assets-only.

  2. Create your single-page application within this Worker project cd <WORKERS PROJECT NAME>, npm create vite@latest .

  3. Configure your wrangler.toml/wrangler.json file to specify your single-page application’s dist or build folder. You also must specify the not_found_handling mode of single-page-application.

    //if using wrangler.toml
    assets = { directory = "./spa-app/dist", not_found_handling = "single-page-application" }
    
    //if using wrangler.json
    "assets": {
        "directory": "./spa-app/dist",
        "not_found_handling": "single-page-application" // only required when there is no Workers code
    }
    

That’s it! That’s how to host single-page applications on Cloudflare Workers, whether you’re hosting only your single-page application or hosting your single-page application with a full Workers project. This GitHub repository contains the sample code showing how to host React, Angular and Vue applications on Workers with static assets, all configuration included.