| name | cloudflare-worker-base |
| description | Set up Cloudflare Workers with Hono routing, Vite plugin, and Static Assets using production-tested patterns.
Prevents 6 errors: export syntax, routing conflicts, HMR crashes, and Service Worker format confusion.
Use when: creating Workers projects, configuring Hono or Vite for Workers, deploying with Wrangler,
adding Static Assets with SPA fallback, or troubleshooting export syntax, API route conflicts, scheduled
handlers, or HMR race conditions.
Keywords: Cloudflare Workers, CF Workers, Hono, wrangler, Vite, Static Assets, @cloudflare/vite-plugin,
wrangler.jsonc, ES Module, run_worker_first, SPA fallback, API routes, serverless, edge computing,
"Cannot read properties of undefined", "Static Assets 404", "A hanging Promise was canceled",
"Handler does not export", deployment fails, routing not working, HMR crashes
|
| license | MIT |
Cloudflare Worker Base Stack
Production-tested: cloudflare-worker-base-test (https://cloudflare-worker-base-test.webfonts.workers.dev)
Last Updated: 2025-10-20
Status: Production Ready ✅
Quick Start (5 Minutes)
1. Scaffold Project
npm create cloudflare@latest my-worker -- \
--type hello-world \
--ts \
--git \
--deploy false \
--framework none
Why these flags:
--type hello-world: Clean starting point
--ts: TypeScript support
--git: Initialize git repo
--deploy false: Don't deploy yet (configure first)
--framework none: We'll add Vite ourselves
2. Install Dependencies
cd my-worker
npm install hono@4.10.1
npm install -D @cloudflare/vite-plugin@1.13.13 vite@^7.0.0
Version Notes:
hono@4.10.1: Latest stable (verified 2025-10-20)
@cloudflare/vite-plugin@1.13.13: Latest stable, fixes HMR race condition
vite: Latest version compatible with Cloudflare plugin
3. Configure Wrangler
Create or update wrangler.jsonc:
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "my-worker",
"main": "src/index.ts",
"account_id": "YOUR_ACCOUNT_ID",
"compatibility_date": "2025-10-11",
"observability": {
"enabled": true
},
"assets": {
"directory": "./public/",
"binding": "ASSETS",
"not_found_handling": "single-page-application",
"run_worker_first": ["/api/*"]
}
}
CRITICAL: run_worker_first Configuration
- Without this, SPA fallback intercepts API routes
- API routes return
index.html instead of JSON
- Source: workers-sdk #8879
4. Configure Vite
Create vite.config.ts:
import { defineConfig } from 'vite'
import { cloudflare } from '@cloudflare/vite-plugin'
export default defineConfig({
plugins: [
cloudflare({
}),
],
})
Why @cloudflare/vite-plugin:
- Official plugin from Cloudflare
- Supports HMR with Workers
- Enables local development with Miniflare
- Version 1.13.13 fixes "A hanging Promise was canceled" error
The Four-Step Setup Process
Step 1: Create Hono App with API Routes
Create src/index.ts:
import { Hono } from 'hono'
type Bindings = {
ASSETS: Fetcher
}
const app = new Hono<{ Bindings: Bindings }>()
app.get('/api/hello', (c) => {
return c.json({
message: 'Hello from Cloudflare Workers!',
timestamp: new Date().toISOString(),
})
})
app.get('/api/health', (c) => {
return c.json({
status: 'ok',
version: '1.0.0',
environment: c.env ? 'production' : 'development',
})
})
app.all('*', (c) => {
return c.env.ASSETS.fetch(c.req.raw)
})
export default app
Why This Export Pattern:
Step 2: Create Static Frontend
Create public/index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Worker App</title>
<link rel="stylesheet" href="/styles.css">
</head>
<body>
<div class="container">
<h1>Cloudflare Worker + Static Assets</h1>
<button onclick="testAPI()">Test API</button>
<pre id="output"></pre>
</div>
<script src="/script.js"></script>
</body>
</html>
Create public/script.js:
async function testAPI() {
const response = await fetch('/api/hello')
const data = await response.json()
document.getElementById('output').textContent = JSON.stringify(data, null, 2)
}
Create public/styles.css:
body {
font-family: system-ui, -apple-system, sans-serif;
max-width: 800px;
margin: 40px auto;
padding: 20px;
}
button {
background: #0070f3;
color: white;
border: none;
padding: 12px 24px;
border-radius: 6px;
cursor: pointer;
}
pre {
background: #f5f5f5;
padding: 16px;
border-radius: 6px;
overflow-x: auto;
}
Step 3: Update Package Scripts
Update package.json:
{
"scripts": {
"dev": "wrangler dev",
"deploy": "wrangler deploy",
"cf-typegen": "wrangler types"
}
}
Step 4: Test & Deploy
npm run cf-typegen
npm run dev
npm run deploy
Known Issues Prevention
This skill prevents 6 documented issues:
Issue #1: Export Syntax Error
Error: "Cannot read properties of undefined (reading 'map')"
Source: honojs/hono #3955
Prevention: Use export default app (NOT { fetch: app.fetch })
Issue #2: Static Assets Routing Conflicts
Error: API routes return index.html instead of JSON
Source: workers-sdk #8879
Prevention: Add "run_worker_first": ["/api/*"] to wrangler.jsonc
Issue #3: Scheduled/Cron Not Exported
Error: "Handler does not export a scheduled() function"
Source: honojs/vite-plugins #275
Prevention: Use Module Worker format when needed:
export default {
fetch: app.fetch,
scheduled: async (event, env, ctx) => { }
}
Issue #4: HMR Race Condition
Error: "A hanging Promise was canceled" during development
Source: workers-sdk #9518
Prevention: Use @cloudflare/vite-plugin@1.13.13 or later
Issue #5: Static Assets Upload Race
Error: Non-deterministic deployment failures in CI/CD
Source: workers-sdk #7555
Prevention: Use Wrangler 4.x+ with retry logic (fixed in recent versions)
Issue #6: Service Worker Format Confusion
Error: Using deprecated Service Worker format
Source: Cloudflare migration guide
Prevention: Always use ES Module format (shown in Step 1)
Configuration Files Reference
wrangler.jsonc (Full Example)
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "my-worker",
"main": "src/index.ts",
"account_id": "YOUR_ACCOUNT_ID",
"compatibility_date": "2025-10-11",
"observability": {
"enabled": true
},
"assets": {
"directory": "./public/",
"binding": "ASSETS",
"not_found_handling": "single-page-application",
"run_worker_first": ["/api/*"]
}
}
Why wrangler.jsonc over wrangler.toml:
- JSON format preferred since Wrangler v3.91.0
- Better IDE support with JSON schema
- Comments allowed with JSONC
vite.config.ts (Full Example)
import { defineConfig } from 'vite'
import { cloudflare } from '@cloudflare/vite-plugin'
export default defineConfig({
plugins: [
cloudflare({
persist: true,
}),
],
server: {
port: 8787,
},
build: {
target: 'esnext',
minify: true,
},
})
tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"lib": ["ES2022"],
"moduleResolution": "bundler",
"types": ["@cloudflare/workers-types/2023-07-01"],
"resolveJsonModule": true,
"allowJs": true,
"checkJs": false,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"noEmit": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
API Route Patterns
Basic JSON Response
app.get('/api/users', (c) => {
return c.json({
users: [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
]
})
})
POST with Request Body
app.post('/api/users', async (c) => {
const body = await c.req.json()
return c.json({ success: true, data: body }, 201)
})
Route Parameters
app.get('/api/users/:id', (c) => {
const id = c.req.param('id')
return c.json({ id, name: 'User' })
})
Query Parameters
app.get('/api/search', (c) => {
const query = c.req.query('q')
return c.json({ query, results: [] })
})
Error Handling
app.get('/api/data', async (c) => {
try {
return c.json({ success: true })
} catch (error) {
return c.json({ error: error.message }, 500)
}
})
Using Bindings (KV, D1, R2)
type Bindings = {
ASSETS: Fetcher
MY_KV: KVNamespace
DB: D1Database
MY_BUCKET: R2Bucket
}
const app = new Hono<{ Bindings: Bindings }>()
app.get('/api/data', async (c) => {
const value = await c.env.MY_KV.get('key')
const result = await c.env.DB.prepare('SELECT * FROM users').all()
const object = await c.env.MY_BUCKET.get('file.txt')
return c.json({ value, result, object })
})
Static Assets Best Practices
Directory Structure
public/
├── index.html # Main entry point
├── styles.css # Global styles
├── script.js # Client-side JavaScript
├── favicon.ico # Favicon
└── assets/ # Images, fonts, etc.
├── logo.png
└── fonts/
SPA Fallback
The "not_found_handling": "single-page-application" configuration means:
- Unknown routes return
index.html
- Useful for React Router, Vue Router, etc.
- BUT requires
run_worker_first for API routes!
Route Priority
With "run_worker_first": ["/api/*"]:
/api/hello → Worker handles it (returns JSON)
/ → Static Assets serve index.html
/styles.css → Static Assets serve styles.css
/unknown → Static Assets serve index.html (SPA fallback)
Caching Static Assets
Static Assets are automatically cached at the edge. To bust cache:
<link rel="stylesheet" href="/styles.css?v=1.0.0">
<script src="/script.js?v=1.0.0"></script>
Development Workflow
Local Development
npm run dev
- Server runs on http://localhost:8787
- HMR enabled (file changes reload automatically)
- Uses Miniflare for local simulation
- All bindings work locally (KV, D1, R2)
Testing API Routes
curl http://localhost:8787/api/hello
curl -X POST http://localhost:8787/api/echo \
-H "Content-Type: application/json" \
-d '{"test": "data"}'
Type Generation
npm run cf-typegen
Generates worker-configuration.d.ts with:
- Binding types (KV, D1, R2, etc.)
- Environment variable types
- Auto-completes in your editor
Deployment
npm run deploy
wrangler deploy --env staging
wrangler tail
wrangler deployments list
Complete Setup Checklist
Advanced Topics
Adding Middleware
import { Hono } from 'hono'
import { logger } from 'hono/logger'
import { cors } from 'hono/cors'
const app = new Hono<{ Bindings: Bindings }>()
app.use('*', logger())
app.use('/api/*', cors())
app.use('/admin/*', async (c, next) => {
await next()
})
Environment-Specific Configuration
{
"name": "my-worker",
"env": {
"staging": {
"vars": { "ENV": "staging" }
},
"production": {
"vars": { "ENV": "production" }
}
}
}
Deploy: wrangler deploy --env staging
Custom Error Pages
app.onError((err, c) => {
console.error(err)
return c.json({ error: 'Internal Server Error' }, 500)
})
app.notFound((c) => {
return c.json({ error: 'Not Found' }, 404)
})
Testing with Vitest
npm install -D vitest @cloudflare/vitest-pool-workers
Create vitest.config.ts:
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
poolOptions: {
workers: {
wrangler: { configPath: './wrangler.jsonc' },
},
},
},
})
See reference/testing.md for complete testing guide.
File Templates
All templates are available in the templates/ directory:
- wrangler.jsonc - Complete Worker configuration
- vite.config.ts - Vite + Cloudflare plugin setup
- package.json - Dependencies and scripts
- tsconfig.json - TypeScript configuration
- src/index.ts - Hono app with API routes
- public/index.html - Static frontend example
- public/styles.css - Example styling
- public/script.js - API test functions
Copy these files to your project and customize as needed.
Reference Documentation
For deeper understanding, see:
- architecture.md - Deep dive into export patterns, routing, and Static Assets
- common-issues.md - All 6 issues with detailed troubleshooting
- deployment.md - Wrangler commands, CI/CD patterns, and production tips
Official Documentation
Dependencies (Latest Verified 2025-10-20)
{
"dependencies": {
"hono": "^4.10.1"
},
"devDependencies": {
"@cloudflare/vite-plugin": "^1.13.13",
"@cloudflare/workers-types": "^4.20251011.0",
"vite": "^7.0.0",
"wrangler": "^4.43.0",
"typescript": "^5.9.0"
}
}
Production Example
This skill is based on the cloudflare-worker-base-test project:
All patterns in this skill have been validated in production.
Questions? Issues?
- Check
reference/common-issues.md first
- Verify all steps in the 4-step setup process
- Ensure
export default app (not { fetch: app.fetch })
- Ensure
run_worker_first is configured
- Check official docs: https://developers.cloudflare.com/workers/