Deploying your SPA to Cloudflare

Serving your files from a designated directory can significantly reduce your hosting costs on Cloudflare as your workers are only charged for request that invoke you worker script directly.

If you have been working in a Single Page Application ( SPA App ) and decide that you would like to host it on Cloudflare worker platform this article will give you an overview of the steps taken.

Traditionally Single Page Applications have, as defined in their name, a single index.html file, request for urls or files outside of that index.html , the routes are then handled within using a router process within the application, this means should you attempt a url something not disclosed within the router eg./user/id/1 the application would return a 404.

In this example we have used create vite app though this could be any framework or flavour of a Single Page Application you favour, and its parsed by way of Transpiling into similar high level code the output of which is usually in the folder ./dist

Adding Support for Cloudflare Workers

pnpm add hono
pnpm add -D wrangler
  • create a wrangler.jsonc file in the root directory ./wrangler.jsonc
{
  "$schema": "node_modules/wrangler/config-schema.json",
  "name": "spa-cf",
  "main": "worker/index.ts",
  "compatibility_date": "2025-09-22",
  "observability": {
    "enabled": true,
  },
  "assets": {
    "directory": "./dist/",
    "not_found_handling": "single-page-application",
  },
}

Update your Package.json

You will need the following scripts added to the package.json file

{
  "scripts": {
    "dev": "vite",
    "w:dev": "wrangler dev",
    "build": "tsc -b && vite build",
    "lint": "eslint .",
    "preview": "vite preview",
    "cf-typegen": "wrangler types --env-interface CloudflareBindings",
    "deploy": "pnpm run build && wrangler deploy"
  }
}

Generate you Typescript types

Run the command.

pnpm cf-typegen

This command adds a worker-configuration.d.ts file in the root of your project with all the necessary types used by your worker to get full type completion.

Create you API

[!TIP] If you don't need a worker and/or an api, you can remove the "main": "worker/index.ts" property from the ./worker.jsonc file, your content will be served from the assets.directory folder instead only.

  • add another config for typescript for the worker at the root ./tsconfig.worker.json
// ./tsconfig.worker.json
{
  "extends": "./tsconfig.node.json",
  "compilerOptions": {
    "types": ["@cloudflare/workers-types", "./worker-configuration.d.ts"],
  },
  "include": ["worker/**/*"],
}
  • include it in the base config file ./tsconfig.json
{
  "files": [],
  "references": [
    { "path": "./tsconfig.app.json" },
    { "path": "./tsconfig.node.json" },
+    { "path": "./tsconfig.worker.json"} // add this one
  ],
}

Create your worker file that you defined in the jsonc file's main property ./worker/index.ts with your hono code.

import { Hono } from 'hono'
const app = new Hono()

app.get('/api/', (c) => c.json({ name: 'leonardo' }))

export default app

Run Wrangler dev server locally

You can now test out the app locally, by building your app for preview:

pnpm build
  • you should now have a ./dist directory with your html,css and js files ready to preview.

  • run the wrangler dev server and open the displayed URL in you browser

pnpm w:dev

Testing Functionality

If you navigate to the root http://127.0.0.1:8787 your app should display correctly.

If you navigate to a page that does not exist http://127.0.0.1:8787/notapage/ you still get the default index.html page, unless you have used an app router to display a custom 404 page within your SPA.

navigating to http://127.0.0.1:8787/api/ still gives you the default index.html page, and this is as it should be, your browser makes the request with the Sec-Fetch-Mode: navigate header set, the server sees this, and bypasses the worker and looks for a .html page, and as it can't find one returns the default index.html instead.

You can try this out with a tool like Postman and add the Sec-Fetch-Mode: navigate header and see the difference if you change it to Sec-Fetch-Mode: cors when you get back the JSON object instead of the index.html page.

Forcing the worker to intercept a route

If you need to make sure a route is always intercepted by a worker you can add a property in the wrangler.jsonc file to do this, if we take our previously created file and add a run_worker_first key with the value of an array of ["/api/*"] to the assets object

{
 "$schema": "node_modules/wrangler/config-schema.json",
 "name": "spa-cf",
 "main": "worker/index.ts",
 "compatibility_date": "2025-09-22",
 "observability": {
  "enabled": true
 },
 "assets": {
    "directory": "./dist/",
    "not_found_handling": "single-page-application"
+   "run_worker_first": ["/api/*"]
  }
}

Deploy to Cloudflare

pnpm deploy

Conclusion

With Vite currently the most popular frontend framework/bundler as of September 2025 for js/ts developers, having somewhere to host your projects for little cost at all on Cloudflare is an attractive proposition.

Thanks for reading.

Contact Us

We are waiting to hear from you

Find us here
Unit 12 NeedSpace
Brighton Road, Horsham
RH13 5BB
Say hello

tellmemore@azydeco.com