Static Assets
You can upload static assets (HTML, CSS, images and other files) as part of your Worker, and Cloudflare will handle caching and serving them to web browsers.
When you deploy your project, Cloudflare deploys both your Worker code and your static assets in a single operation. This deployment operates as a tightly integrated "unit" running across Cloudflare's network, combining static file hosting, custom logic, and global caching.
The assets directory specified in your Wrangler configuration file is central to this design. During deployment, Wrangler automatically uploads the files from this directory to Cloudflare's infrastructure. Once deployed, requests for these assets are routed efficiently to locations closest to your users.
{ "name": "my-spa", "main": "src/index.js", "compatibility_date": "2025-01-01", "assets": { "directory": "./dist", "binding": "ASSETS" }}
name = "my-spa" main = "src/index.js" compatibility_date = "2025-01-01" [assets] directory = "./dist" binding = "ASSETS"
By adding an assets binding, you can directly fetch and serve assets within your Worker code.
// index.js
export default { async fetch(request, env) { const url = new URL(request.url);
if (url.pathname.startsWith("/api/")) { return new Response(JSON.stringify({ name: "Cloudflare" }), { headers: { "Content-Type": "application/json" }, }); }
return env.ASSETS.fetch(request); },};
By default, if a requested URL matches a file in the static assets directory, that file will be served — without invoking Worker code. If no matching asset is found and a Worker script is present, the request will be processed by the Worker. The Worker can return a response or choose to defer again to static assets by using the assets binding (e.g. env.ASSETS.fetch(request)
). If no Worker script is present, a 404 Not Found
response is returned.
The default behavior for requests which don't match a static asset can be changed by setting the not_found_handling
option under assets
in your Wrangler configuration file:
not_found_handling = "single-page-application"
: Sets your application to return a200 OK
response withindex.html
for requests which don't match a static asset. Use this if you have a Single Page Application.not_found_handling = "404-page"
: Sets your application to return a404 Not Found
response with the nearest404.html
for requests which don't match a static asset.
{ "assets": { "directory": "./dist", "not_found_handling": "single-page-application" }}
[assets] directory = "./dist" not_found_handling = "single-page-application"
If you want the Worker code to execute before serving an asset (for example, to protect an asset behind authentication), you can set run_worker_first = true
.
{ "assets": { "directory": "./dist", "run_worker_first": true }}
[assets] directory = "./dist" run_worker_first = true
Cloudflare provides automatic caching for static assets across its network, ensuring fast delivery to users worldwide. When a static asset is requested, it is automatically cached for future requests.
-
First Request: When an asset is requested for the first time, it is fetched from storage and cached at the nearest Cloudflare location.
-
Subsequent Requests: If a request for the same asset reaches a data center that does not have it cached, Cloudflare's tiered caching system allows it to be retrieved from a nearby cache rather than going back to storage. This improves cache hit ratio, reduces latency, and reduces unnecessary origin fetches.
npm create cloudflare@latest -- my-dynamic-site
For setup, select the following options:
- For What would you like to start with?, choose
Framework
. - For Which framework would you like to use?, choose
React
. - For Which language do you want to use?, choose
TypeScript
. - For Do you want to use git for version control?, choose
Yes
. - For Do you want to deploy your application?, choose
No
(we will be making some changes before deploying).
After setting up the project, change the directory by running the following command:
cd my-dynamic-site
Run the following command to build the project:
npm run build
We should now see a new directory /dist
in our project, which contains the compiled assets:
- package.json
- index.html
- ...
Directorydist Asset directory
- ... Compiled assets
Directorysrc
- ...
- ...
In the next step, we use a Wrangler configuration file to allow Cloudflare to locate our compiled assets.
{ "name": "my-spa", "compatibility_date": "2025-01-01", "assets": { "directory": "./dist" }}
name = "my-spa" compatibility_date = "2025-01-01" [assets] directory = "./dist"
Notice the [assets]
block: here we have specified our directory where Cloudflare can find our compiled assets (./dist
).
Our project structure should now look like this:
- package.json
- index.html
- wrangler.toml Wrangler configuration
- ...
Directorydist Asset directory
- ... Compiled assets
Directorysrc
- ...
- ...
npx wrangler deploy
Our project is now deployed on Workers! But we can take this even further, by adding an API Worker.
Replace the file contents of our Wrangler configuration with the following:
{ "name": "my-spa", "main": "src/api/index.js", "compatibility_date": "2025-01-01", "assets": { "directory": "./dist", "binding": "ASSETS", "not_found_handling": "single-page-application" }}
name = "my-spa" main = "src/api/index.js" compatibility_date = "2025-01-01" [assets] directory = "./dist" binding = "ASSETS" not_found_handling = "single-page-application"
We have edited the Wrangler file in the following ways:
- Added
main = "src/api/index.js"
to tell Cloudflare where to find our Worker code. - Added an
ASSETS
binding, which our Worker code can use to fetch and serve assets. - Enabled routing for Single Page Applications, which ensures that unmatched routes (such as
/dashboard
) serve ourindex.html
.
Copy the contents below into the index.js file.
// api/index.js
export default { async fetch(request, env) { const url = new URL(request.url);
if (url.pathname.startsWith("/api/")) { return new Response(JSON.stringify({ name: "Cloudflare" }), { headers: { "Content-Type": "application/json" }, }); }
return env.ASSETS.fetch(request); },};
Consider what this Worker does:
- Our Worker receives a HTTP request and extracts the URL.
- If the request is for an API route (
/api/...
), it returns a JSON response. - Otherwise, it serves static assets from our directory (
env.ASSETS
).
Edit src/App.tsx
so that it includes an additional button that calls the API, and sets some state. Replace the file contents with the following code:
// src/App.tsximport { useState } from "react";import reactLogo from "./assets/react.svg";import viteLogo from "/vite.svg";import "./App.css";
function App() { const [count, setCount] = useState(0); const [name, setName] = useState("unknown");
return ( <> <div> <a href="https://vite.dev" target="_blank"> <img src={viteLogo} className="logo" alt="Vite logo" /> </a> <a href="https://react.dev" target="_blank"> <img src={reactLogo} className="logo react" alt="React logo" /> </a> </div> <h1>Vite + React</h1> <div className="card"> <button onClick={() => setCount((count) => count + 1)} aria-label="increment" > count is {count} </button> <p> Edit <code>src/App.tsx</code> and save to test HMR </p> </div> <div className="card"> <button onClick={() => { fetch("/api/") .then((res) => res.json() as Promise<{ name: string }>) .then((data) => setName(data.name)); }} aria-label="get name" > Name from API is: {name} </button> <p> Edit <code>api/index.ts</code> to change the name </p> </div> <p className="read-the-docs"> Click on the Vite and React logos to learn more </p> </> );}
export default App;
Before deploying again, we need to rebuild our project:
npm run build
npx wrangler deploy
Now we can see a new button Name from API, and if you click the button, we can see our API response as Cloudflare!
Was this helpful?
- Resources
- API
- New to Cloudflare?
- Products
- Sponsorships
- Open Source
- Support
- Help Center
- System Status
- Compliance
- GDPR
- Company
- cloudflare.com
- Our team
- Careers
- 2025 Cloudflare, Inc.
- Privacy Policy
- Terms of Use
- Report Security Issues
- Trademark