Skip to content

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

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"
}
}

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

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, a 404 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 the not_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"
}
}

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
}
}

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

1. Create a new Worker project

Terminal window
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:

Terminal window
cd my-dynamic-site

2. Build project

Run the following command to build the project:

Terminal window
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)

{
"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

Terminal window
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

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"
}
}

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 our index.html.

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

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.tsx
import { 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:

Terminal window
npm run build

7. Deploy with Wrangler

Terminal window
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