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.
How it works
Section titled “How it works”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); },};
Routing behavior
Section titled “Routing behavior”By default, if a requested URL matches a file in the static assets directory, that file will always be served — without running Worker code. If no matching asset is found and a Worker is configured, the request will be processed by the Worker instead.
-
If no Worker is set up, the
not_found_handling
setting in your Wrangler configuration determines what happens next. By default, a404 Not Found
response is returned. -
If a Worker is configured and a request does not match a static asset, the Worker will handle the request. The Worker can choose to pass the request to the asset binding (through
env.ASSETS.fetch()
), following thenot_found_handling
rules.
You can configure and override this default routing behaviour. For example, if you have a Single Page Application and want to serve index.html
for all unmatched routes, you can set not_found_handling = "single-page-application"
:
{ "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
Caching behavior
Section titled “Caching behavior”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.
Try it out
Section titled “Try it out”1. Create a new Worker project
Section titled “1. Create a new Worker project”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
2. Build project
Section titled “2. Build project”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.
3. Add a Wrangler configuration file (wrangler.toml
or wrangler.json
)
Section titled “3. Add a Wrangler configuration file (wrangler.toml or wrangler.json)”{ "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
- ...
- ...
4. Deploy with Wrangler
Section titled “4. Deploy with Wrangler”npx wrangler deploy
Our project is now deployed on Workers! But we can take this even further, by adding an API Worker.
5. Adjust our Wrangler configuration
Section titled “5. Adjust our Wrangler configuration”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
.
5. Create a new directory /api
, and add an index.js
file
Section titled “5. Create a new directory /api, and add an index.js file”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
).
6. Call the API from the client
Section titled “6. Call the API from the client”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
7. Deploy with Wrangler
Section titled “7. Deploy with Wrangler”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!
Learn more
Section titled “Learn more”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