When developing with React Router v7 and Cloudflare Workers, there's a frustrating problem many developers encounter: files in the public directory work perfectly in development (npm run dev
) but return 404 errors when deployed to Cloudflare Workers.
In this article, I'll share the problem I faced and walk you through a practical solution using Static Assets with automatic fallback handling.
The Problem
Works in Development, Fails in Production
In my React Router application, I had JSON files and other static assets in the public
directory, accessing them like this:
// Works perfectly in development
const response = await fetch('/data/config.json');
const config = await response.json();
But after deploying to Cloudflare Workers:
Error: Failed to fetch '/data/config.json' - 404 Not Found
Why This Happens
The root cause is the difference in how files are served between development and production environments:
- Development (Vite): Files in the
public
directory are automatically served at the root path - Cloudflare Workers: Static files need to be configured separately as Static Assets
So while /data/config.json
works in development, the same path isn't accessible in production.
The Solution: Static Assets + Fallback Handling
To solve this problem, I developed a solution combining these elements:
- Cloudflare Static Assets integration
- Automatic environment-based fallback
- Type-safe utility functions
1. Configuring wrangler.jsonc
First, enable Static Assets in Cloudflare Workers:
{
"compatibility_date": "2024-11-18",
"assets": {
"directory": "./public",
"binding": "ASSETS"
}
}
2. Implementing the Utility Functions
Next, create utilities that access files appropriately based on the environment:
/**
* Static Assets Utility
*
* Provides unified access to static files across different environments:
* - Development (Vite): Uses standard fetch()
* - Production (Cloudflare Workers): Uses ASSETS binding
* - Build time (Node.js): Falls back to file system access
*/
interface CloudflareContext {
cloudflare?: {
env: Env;
};
}
/**
* Determines if we have access to Cloudflare Workers ASSETS binding
*/
function hasAssetsBinding(context?: CloudflareContext): boolean {
return !!context?.cloudflare?.env?.ASSETS;
}
/**
* Fetches a static asset with automatic fallback between ASSETS and standard fetch
*/
export async function fetchStaticAsset(
path: string,
context?: CloudflareContext,
request?: Request,
): Promise<Response> {
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
// Try Static Assets first if available
if (hasAssetsBinding(context) && context?.cloudflare?.env?.ASSETS) {
try {
const assetUrl = new URL(normalizedPath, "https://example.com");
const assetRequest = new Request(assetUrl.toString());
const response = await context.cloudflare.env.ASSETS.fetch(assetRequest);
if (response.ok) {
return response;
}
// Log fallback only in development
if (
typeof process !== "undefined" &&
process.env.NODE_ENV === "development"
) {
console.warn(
`[Static Assets] Failed for ${normalizedPath} (${response.status}), using fallback`,
);
}
} catch (error) {
// Log errors only in development
if (
typeof process !== "undefined" &&
process.env.NODE_ENV === "development"
) {
console.warn(`[Static Assets] Error for ${normalizedPath}:`, error);
}
}
}
// Fallback: Use standard fetch
let url = normalizedPath;
if (request) {
const requestUrl = new URL(request.url);
url = `${requestUrl.origin}${normalizedPath}`;
}
const response = await fetch(url);
if (!response.ok) {
throw new Error(
`Static asset not found: ${normalizedPath} (${response.status})`,
);
}
return response;
}
3. Type-Safe Helper Functions
I also provide convenient helper functions for JSON and text files:
/**
* Fetches and parses a JSON static asset
*/
export async function fetchStaticJSON<T>(
path: string,
context?: CloudflareContext,
request?: Request,
): Promise<T> {
try {
const response = await fetchStaticAsset(path, context, request);
const json = await response.json();
return json as T;
} catch (error) {
// Log detailed errors only in development
if (
typeof process !== "undefined" &&
process.env.NODE_ENV === "development"
) {
console.error(`Failed to fetch static JSON ${path}:`, error);
}
throw new Error(`Failed to load JSON asset: ${path}`);
}
}
/**
* Fetches a text static asset
*/
export async function fetchStaticText(
path: string,
context?: CloudflareContext,
request?: Request,
): Promise<string> {
try {
const response = await fetchStaticAsset(path, context, request);
const text = await response.text();
return text;
} catch (error) {
// Log detailed errors only in development
if (
typeof process !== "undefined" &&
process.env.NODE_ENV === "development"
) {
console.error(`Failed to fetch static text ${path}:`, error);
}
throw new Error(`Failed to load text asset: ${path}`);
}
}
Real-World Usage Examples
Using in React Router Loaders
import type { Route } from './+types/route';
import { fetchStaticJSON } from '~/lib/utils/static-assets';
interface AppConfig {
apiUrl: string;
features: string[];
}
export async function loader({ request, context }: Route.LoaderArgs) {
// Same code works in all environments
const config = await fetchStaticJSON<AppConfig>(
'/data/config.json',
context,
request
);
return { config };
}
export default function ConfigPage({ loaderData }: Route.ComponentProps) {
const { config } = loaderData;
return (
<div>
<h1>App Configuration</h1>
<p>API URL: {config.apiUrl}</p>
<ul>
{config.features.map(feature => (
<li key={feature}>{feature}</li>
))}
</ul>
</div>
);
}
React Router-Specific Helpers
For easier use with React Router, I also provide dedicated helper functions:
/**
* React Router specific helper for fetching static JSON
*/
export async function fetchStaticJSONFromRoute<T>(
path: string,
context: ReactRouterContext,
request?: Request,
): Promise<T> {
return fetchStaticJSON<T>(path, context, request);
}
/**
* React Router specific helper for fetching static text
*/
export async function fetchStaticTextFromRoute(
path: string,
context: ReactRouterContext,
request?: Request,
): Promise<string> {
return fetchStaticText(path, context, request);
}
Benefits of This Solution
1. Environment-Agnostic Unified API
The same code works in both development and production environments. You don't need to worry about environment differences.
2. Automatic Fallback Functionality
When Static Assets aren't available or fail, the system automatically falls back to standard fetch.
3. Type Safety
Leverages TypeScript's type system to handle JSON file structures in a type-safe manner.
4. Proper Error Handling
Outputs detailed logs in development while suppressing unnecessary logs in production.
5. Performance Optimization
Prioritizes Cloudflare Static Assets for fast file delivery at the edge.
Important Considerations
Security
Make sure files you don't want to be public aren't included in the public directory.
Conclusion
The static assets problem with Cloudflare Workers + React Router is a challenge many developers face. However, by combining Static Assets with fallback functionality, we can solve it reliably.
This solution provides:
- Better Developer Experience: Develop without worrying about environment differences
- Improved Reliability: Robustness through automatic fallback
- Performance Optimization: Fast file delivery at the edge
I hope this helps anyone struggling with the same issue. If you have questions or suggestions for improvements, please let me know in the comments!