
Cloudflare Worker Base
- 39 installs
- 16 repo stars
- Updated November 20, 2025
- jackspace/claudeskillz
Set up a production Cloudflare Workers project with Hono routing, Vite, Static Assets, and Wrangler deployment.
About
A production-tested base setup for Cloudflare Workers using Hono, Vite, and Static Assets. A developer uses it when starting a new Workers project or configuring routing, static assets, and Wrangler deploys.
- Hono routing plus Vite plugin and Static Assets setup
- Prevents 6 issues like export syntax errors and Static Assets routing conflicts
Cloudflare Worker Base by the numbers
- 39 all-time installs (skills.sh)
- Ranked #761 of 1,039 Cloud & Infrastructure skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/jackspace/claudeskillz --skill cloudflare-worker-baseAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 39 |
|---|---|
| repo stars | ★ 16 |
| Last updated | November 20, 2025 |
| Repository | jackspace/claudeskillz ↗ |
What it does
Set up a production Cloudflare Workers project with Hono routing, Vite, Static Assets, and Wrangler deployment.
Files
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 noneWhy 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@latestVersion Notes:
hono@4.10.1: Latest stable (verified 2025-10-20)@cloudflare/vite-plugin@1.13.13: Latest stable, fixes HMR race conditionvite: 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.htmlinstead 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({
// Optional: Configure the plugin if needed
}),
],
})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:
/**
* Cloudflare Worker with Hono
*
* CRITICAL: Export pattern to prevent build errors
* ✅ CORRECT: export default app
* ❌ WRONG: export default { fetch: app.fetch }
*/
import { Hono } from 'hono'
// Type-safe environment bindings
type Bindings = {
ASSETS: Fetcher
}
const app = new Hono<{ Bindings: Bindings }>()
/**
* API Routes
* Handled BEFORE static assets due to run_worker_first config
*/
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',
})
})
/**
* Fallback to Static Assets
* Any route not matched above is served from public/ directory
*/
app.all('*', (c) => {
return c.env.ASSETS.fetch(c.req.raw)
})
/**
* Export the Hono app directly (ES Module format)
* This is the correct pattern for Cloudflare Workers with Hono + Vite
*/
export default appWhy This Export Pattern:
- Source: honojs/hono #3955
- Using
{ fetch: app.fetch }causes: "Cannot read properties of undefined (reading 'map')" - Exception: If you need scheduled/tail handlers, use Module Worker format:
export default {
fetch: app.fetch,
scheduled: async (event, env, ctx) => { /* ... */ }
}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
# Generate TypeScript types for bindings
npm run cf-typegen
# Start local dev server (http://localhost:8787)
npm run dev
# Deploy to production
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/*"]
}
/* Optional: Environment Variables */
// "vars": { "MY_VARIABLE": "production_value" }
/* Optional: KV Namespace Bindings */
// "kv_namespaces": [
// { "binding": "MY_KV", "id": "YOUR_KV_ID" }
// ]
/* Optional: D1 Database Bindings */
// "d1_databases": [
// { "binding": "DB", "database_name": "my-db", "database_id": "YOUR_DB_ID" }
// ]
/* Optional: R2 Bucket Bindings */
// "r2_buckets": [
// { "binding": "MY_BUCKET", "bucket_name": "my-bucket" }
// ]
}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 state between HMR updates
persist: true,
}),
],
// Optional: Configure server
server: {
port: 8787,
},
// Optional: Build optimizations
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()
// Validate and process body
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 {
// Your logic here
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) => {
// KV
const value = await c.env.MY_KV.get('key')
// D1
const result = await c.env.DB.prepare('SELECT * FROM users').all()
// R2
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_firstfor API routes!
Route Priority
With "run_worker_first": ["/api/*"]:
1. /api/hello → Worker handles it (returns JSON) 2. / → Static Assets serve index.html 3. /styles.css → Static Assets serve styles.css 4. /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
# Test GET endpoint
curl http://localhost:8787/api/hello
# Test POST endpoint
curl -X POST http://localhost:8787/api/echo \
-H "Content-Type: application/json" \
-d '{"test": "data"}'Type Generation
npm run cf-typegenGenerates worker-configuration.d.ts with:
- Binding types (KV, D1, R2, etc.)
- Environment variable types
- Auto-completes in your editor
Deployment
# Deploy to production
npm run deploy
# Deploy to specific environment
wrangler deploy --env staging
# Tail logs in production
wrangler tail
# Check deployment status
wrangler deployments list---
Complete Setup Checklist
- [ ] Project scaffolded with
npm create cloudflare@latest - [ ] Dependencies installed:
hono@4.10.1,@cloudflare/vite-plugin@1.13.13 - [ ]
wrangler.jsonccreated with: - [ ]
account_idset to your Cloudflare account - [ ]
assets.directorypointing to./public/ - [ ]
assets.run_worker_firstincludes/api/* - [ ]
compatibility_dateset to recent date - [ ]
vite.config.tscreated with@cloudflare/vite-plugin - [ ]
src/index.tscreated with Hono app - [ ] Uses
export default app(NOT{ fetch: app.fetch }) - [ ] Includes ASSETS binding type
- [ ] Has fallback route:
app.all('*', (c) => c.env.ASSETS.fetch(c.req.raw)) - [ ]
public/directory created with static files - [ ]
npm run cf-typegenexecuted successfully - [ ]
npm run devstarts without errors - [ ] API routes tested in browser/curl
- [ ] Static assets serve correctly
- [ ] HMR works without crashes
- [ ] Ready to deploy with
npm run deploy
---
Advanced Topics
Adding Middleware
import { Hono } from 'hono'
import { logger } from 'hono/logger'
import { cors } from 'hono/cors'
const app = new Hono<{ Bindings: Bindings }>()
// Global middleware
app.use('*', logger())
app.use('/api/*', cors())
// Route-specific middleware
app.use('/admin/*', async (c, next) => {
// Auth check
await next()
})Environment-Specific Configuration
// wrangler.jsonc
{
"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-workersCreate 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
- Cloudflare Workers: https://developers.cloudflare.com/workers/
- Static Assets: https://developers.cloudflare.com/workers/static-assets/
- Vite Plugin: https://developers.cloudflare.com/workers/vite-plugin/
- Wrangler Configuration: https://developers.cloudflare.com/workers/wrangler/configuration/
- Hono: https://hono.dev/docs/getting-started/cloudflare-workers
- Context7 Library ID:
/websites/developers_cloudflare-workers
---
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:
- Live: https://cloudflare-worker-base-test.webfonts.workers.dev
- Build Time: ~45 minutes (actual)
- Errors: 0 (all 6 known issues prevented)
- Validation: ✅ Local dev, HMR, production deployment all successful
All patterns in this skill have been validated in production.
---
Questions? Issues?
1. Check reference/common-issues.md first 2. Verify all steps in the 4-step setup process 3. Ensure export default app (not { fetch: app.fetch }) 4. Ensure run_worker_first is configured 5. Check official docs: https://developers.cloudflare.com/workers/
Cloudflare Worker Base Stack
Status: ✅ Production Ready Last Updated: 2025-10-20 Production Example: https://cloudflare-worker-base-test.webfonts.workers.dev
---
What This Skill Does
Sets up production-ready Cloudflare Workers projects with:
- Hono v4.10.1 for routing
- @cloudflare/vite-plugin v1.13.13 for development
- Workers Static Assets for frontend files
- ES Module format (correct export pattern)
- wrangler.jsonc configuration (preferred over .toml)
Prevents 6 documented issues that commonly break Workers projects.
---
Auto-Trigger Keywords
Claude automatically uses this skill when you mention:
Technologies
- Cloudflare Workers
- CF Workers
- Hono
- Wrangler
- Workers Static Assets
- @cloudflare/vite-plugin
- wrangler.jsonc
- ES Module Worker
- Serverless Cloudflare
- Edge computing
Use Cases
- Create Cloudflare Worker
- Set up Hono routing
- Configure Vite for Workers
- Add Static Assets to Worker
- Deploy with Wrangler
- Initialize Workers project
- Scaffold CF Worker
- Workers with frontend
Common Errors (Triggers Prevention)
- "Cannot read properties of undefined"
- "Static Assets 404"
- "A hanging Promise was canceled"
- "Handler does not export"
- API routes return HTML instead of JSON
- SPA fallback intercepts API
- Deployment fails non-deterministically
- HMR crashes during development
- Routing not working
- Build fails with Vite
---
Known Issues Prevented
| Issue | Error Message | Source | Prevention |
|---|---|---|---|
| #1: Export Syntax | "Cannot read properties of undefined (reading 'map')" | hono #3955 | Use export default app |
| #2: Routing Conflicts | API routes return index.html | workers-sdk #8879 | run_worker_first: ["/api/*"] |
| #3: Scheduled Handler | "Handler does not export scheduled()" | vite-plugins #275 | Module Worker format when needed |
| #4: HMR Race | "A hanging Promise was canceled" | workers-sdk #9518 | Use vite-plugin@1.13.13+ |
| #5: Upload Race | Non-deterministic deployment failures | workers-sdk #7555 | Wrangler 4.x+ with retry |
| #6: Format Confusion | Using deprecated Service Worker format | Cloudflare migration guide | ES Module format |
---
Quick Start
# 1. Scaffold project
npm create cloudflare@latest my-worker -- --type hello-world --ts --git --deploy false --framework none
# 2. Install dependencies
cd my-worker
npm install hono@4.10.1
npm install -D @cloudflare/vite-plugin@1.13.13 vite@latest
# 3. Configure (see SKILL.md for full setup)
# 4. Dev & Deploy
npm run dev
npm run deployFull instructions: See SKILL.md
---
Token Savings Estimate
Without this skill: ~8,000 tokens (documentation lookups + trial-and-error fixing errors) With this skill: ~3,000 tokens (direct implementation with correct patterns)
Savings: ~60% token reduction + prevents all 6 common errors on first attempt
---
File Structure
cloudflare-worker-base/
├── SKILL.md # Full instructions (read this first)
├── README.md # This file
├── templates/ # Copy-ready files
│ ├── wrangler.jsonc # Worker configuration
│ ├── vite.config.ts # Vite + plugin setup
│ ├── package.json # Dependencies
│ ├── tsconfig.json # TypeScript config
│ ├── src/
│ │ └── index.ts # Hono app with API routes
│ └── public/ # Static assets example
│ ├── index.html
│ ├── styles.css
│ └── script.js
└── reference/ # Deep-dive docs
├── architecture.md # Export patterns, routing, Static Assets
├── common-issues.md # All 6 issues with troubleshooting
└── deployment.md # Wrangler, CI/CD, production tips---
Package Versions (Verified 2025-10-20)
| Package | Version | Status |
|---|---|---|
| wrangler | 4.43.0 | ✅ Latest stable |
| @cloudflare/workers-types | 4.20251011.0 | ✅ Latest |
| hono | 4.10.1 | ✅ Latest stable |
| @cloudflare/vite-plugin | 1.13.13 | ✅ Latest (fixes HMR) |
| vite | Latest | ✅ Compatible |
| typescript | 5.9.0+ | ✅ Standard |
---
When to Use This Skill
✅ Use this skill when:
- Creating a new Cloudflare Workers project
- Adding Vite to an existing Worker
- Setting up Hono routing
- Configuring Static Assets with Workers
- Encountering any of the 6 documented errors
- Need production-ready Worker template
- Want to avoid common Workers pitfalls
❌ Don't use this skill for:
- Cloudflare Pages projects (use Workers with Static Assets instead)
- Adding D1/KV/R2 bindings (covered in separate
cloudflare-servicesskill) - Authentication setup (covered in
clerk-authskill) - React/Vue framework setup (those have dedicated skills)
---
Official Documentation
- Cloudflare Workers: https://developers.cloudflare.com/workers/
- Static Assets: https://developers.cloudflare.com/workers/static-assets/
- Vite Plugin: https://developers.cloudflare.com/workers/vite-plugin/
- Hono: https://hono.dev/docs/getting-started/cloudflare-workers
- Wrangler Configuration: https://developers.cloudflare.com/workers/wrangler/configuration/
---
Research Validation
- ✅ All packages verified on npm (2025-10-20)
- ✅ All 6 issues have GitHub issue sources
- ✅ Working example deployed: https://cloudflare-worker-base-test.webfonts.workers.dev
- ✅ Build time: ~45 minutes (0 errors)
- ✅ Research log:
/home/jez/Documents/claude-skills/planning/research-logs/cloudflare-worker-base.md
---
Next Steps After Using This Skill
1. Add Database: Use cloudflare-services skill to add D1/KV/R2 2. Add Authentication: Use clerk-auth skill for user management 3. Add Frontend Framework: Use vite-react or vite-vue skills 4. Set up CI/CD: See reference/deployment.md for GitHub Actions examples
---
Production Tested: ✅ Error Rate: 0 Build Success Rate: 100% Token Efficiency: ~60% savings
Ready to use! Start with SKILL.md for full setup instructions.
{
"description": "|",
"metadata": {
"license": "MIT"
},
"references": {
"files": [
"references/architecture.md",
"references/common-issues.md",
"references/deployment.md"
]
},
"content": "**Production-tested**: cloudflare-worker-base-test (https://cloudflare-worker-base-test.webfonts.workers.dev)\r\n**Last Updated**: 2025-10-20\r\n**Status**: Production Ready ✅\r\n\r\n---\r\n\r\n\r\n### Step 1: Create Hono App with API Routes\r\n\r\nCreate `src/index.ts`:\r\n\r\n```typescript\r\n/**\r\n * Cloudflare Worker with Hono\r\n *\r\n * CRITICAL: Export pattern to prevent build errors\r\n * ✅ CORRECT: export default app\r\n * ❌ WRONG: export default { fetch: app.fetch }\r\n */\r\n\r\nimport { Hono } from 'hono'\r\n\r\n// Type-safe environment bindings\r\ntype Bindings = {\r\n ASSETS: Fetcher\r\n}\r\n\r\nconst app = new Hono<{ Bindings: Bindings }>()\r\n\r\n/**\r\n * API Routes\r\n * Handled BEFORE static assets due to run_worker_first config\r\n */\r\napp.get('/api/hello', (c) => {\r\n return c.json({\r\n message: 'Hello from Cloudflare Workers!',\r\n timestamp: new Date().toISOString(),\r\n })\r\n})\r\n\r\napp.get('/api/health', (c) => {\r\n return c.json({\r\n status: 'ok',\r\n version: '1.0.0',\r\n environment: c.env ? 'production' : 'development',\r\n })\r\n})\r\n\r\n/**\r\n * Fallback to Static Assets\r\n * Any route not matched above is served from public/ directory\r\n */\r\napp.all('*', (c) => {\r\n return c.env.ASSETS.fetch(c.req.raw)\r\n})\r\n\r\n/**\r\n * Export the Hono app directly (ES Module format)\r\n * This is the correct pattern for Cloudflare Workers with Hono + Vite\r\n */\r\nexport default app\r\n```\r\n\r\n**Why This Export Pattern:**\r\n- Source: [honojs/hono #3955](https://github.com/honojs/hono/issues/3955)\r\n- Using `{ fetch: app.fetch }` causes: \"Cannot read properties of undefined (reading 'map')\"\r\n- Exception: If you need scheduled/tail handlers, use Module Worker format:\r\n ```typescript\r\n export default {\r\n fetch: app.fetch,\r\n scheduled: async (event, env, ctx) => { /* ... */ }\r\n }\r\n ```\r\n\r\n### Step 2: Create Static Frontend\r\n\r\nCreate `public/index.html`:\r\n\r\n```html\r\n<!DOCTYPE html>\r\n<html lang=\"en\">\r\n<head>\r\n <meta charset=\"UTF-8\">\r\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\r\n <title>My Worker App</title>\r\n <link rel=\"stylesheet\" href=\"/styles.css\">\r\n</head>\r\n<body>\r\n <div class=\"container\">\r\n <h1>Cloudflare Worker + Static Assets</h1>\r\n <button onclick=\"testAPI()\">Test API</button>\r\n <pre id=\"output\"></pre>\r\n </div>\r\n <script src=\"/script.js\"></script>\r\n</body>\r\n</html>\r\n```\r\n\r\nCreate `public/script.js`:\r\n\r\n```javascript\r\nasync function testAPI() {\r\n const response = await fetch('/api/hello')\r\n const data = await response.json()\r\n document.getElementById('output').textContent = JSON.stringify(data, null, 2)\r\n}\r\n```\r\n\r\nCreate `public/styles.css`:\r\n\r\n```css\r\nbody {\r\n font-family: system-ui, -apple-system, sans-serif;\r\n max-width: 800px;\r\n margin: 40px auto;\r\n padding: 20px;\r\n}\r\n\r\nbutton {\r\n background: #0070f3;\r\n color: white;\r\n border: none;\r\n padding: 12px 24px;\r\n border-radius: 6px;\r\n cursor: pointer;\r\n}\r\n\r\npre {\r\n background: #f5f5f5;\r\n padding: 16px;\r\n border-radius: 6px;\r\n overflow-x: auto;\r\n}\r\n```\r\n\r\n### Step 3: Update Package Scripts\r\n\r\nUpdate `package.json`:\r\n\r\n```json\r\n{\r\n \"scripts\": {\r\n \"dev\": \"wrangler dev\",\r\n \"deploy\": \"wrangler deploy\",\r\n \"cf-typegen\": \"wrangler types\"\r\n }\r\n}\r\n```\r\n\r\n### Step 4: Test & Deploy\r\n\r\n```bash\r\nnpm run cf-typegen\r\n\r\nnpm run dev\r\n\r\n\r\n### Local Development\r\n\r\n```bash\r\nnpm run dev\r\n```\r\n\r\n- Server runs on http://localhost:8787\r\n- HMR enabled (file changes reload automatically)\r\n- Uses Miniflare for local simulation\r\n- All bindings work locally (KV, D1, R2)\r\n\r\n### Testing API Routes\r\n\r\n```bash\r\ncurl http://localhost:8787/api/hello\r\n\r\ncurl -X POST http://localhost:8787/api/echo \\\r\n -H \"Content-Type: application/json\" \\\r\n -d '{\"test\": \"data\"}'\r\n```\r\n\r\n### Type Generation\r\n\r\n```bash\r\nnpm run cf-typegen\r\n```\r\n\r\nGenerates `worker-configuration.d.ts` with:\r\n- Binding types (KV, D1, R2, etc.)\r\n- Environment variable types\r\n- Auto-completes in your editor\r\n\r\n### Deployment\r\n\r\n```bash\r\nnpm run deploy\r\n\r\nwrangler deploy --env staging\r\n\r\nwrangler tail",
"name": "cloudflare-worker-base",
"id": "cloudflare-worker-base",
"sections": {
"Quick Start (5 Minutes)": "### 1. Scaffold Project\r\n\r\n```bash\r\nnpm create cloudflare@latest my-worker -- \\\r\n --type hello-world \\\r\n --ts \\\r\n --git \\\r\n --deploy false \\\r\n --framework none\r\n```\r\n\r\n**Why these flags:**\r\n- `--type hello-world`: Clean starting point\r\n- `--ts`: TypeScript support\r\n- `--git`: Initialize git repo\r\n- `--deploy false`: Don't deploy yet (configure first)\r\n- `--framework none`: We'll add Vite ourselves\r\n\r\n### 2. Install Dependencies\r\n\r\n```bash\r\ncd my-worker\r\nnpm install hono@4.10.1\r\nnpm install -D @cloudflare/vite-plugin@1.13.13 vite@latest\r\n```\r\n\r\n**Version Notes:**\r\n- `hono@4.10.1`: Latest stable (verified 2025-10-20)\r\n- `@cloudflare/vite-plugin@1.13.13`: Latest stable, fixes HMR race condition\r\n- `vite`: Latest version compatible with Cloudflare plugin\r\n\r\n### 3. Configure Wrangler\r\n\r\nCreate or update `wrangler.jsonc`:\r\n\r\n```jsonc\r\n{\r\n \"$schema\": \"node_modules/wrangler/config-schema.json\",\r\n \"name\": \"my-worker\",\r\n \"main\": \"src/index.ts\",\r\n \"account_id\": \"YOUR_ACCOUNT_ID\",\r\n \"compatibility_date\": \"2025-10-11\",\r\n \"observability\": {\r\n \"enabled\": true\r\n },\r\n \"assets\": {\r\n \"directory\": \"./public/\",\r\n \"binding\": \"ASSETS\",\r\n \"not_found_handling\": \"single-page-application\",\r\n \"run_worker_first\": [\"/api/*\"]\r\n }\r\n}\r\n```\r\n\r\n**CRITICAL: `run_worker_first` Configuration**\r\n- Without this, SPA fallback intercepts API routes\r\n- API routes return `index.html` instead of JSON\r\n- Source: [workers-sdk #8879](https://github.com/cloudflare/workers-sdk/issues/8879)\r\n\r\n### 4. Configure Vite\r\n\r\nCreate `vite.config.ts`:\r\n\r\n```typescript\r\nimport { defineConfig } from 'vite'\r\nimport { cloudflare } from '@cloudflare/vite-plugin'\r\n\r\nexport default defineConfig({\r\n plugins: [\r\n cloudflare({\r\n // Optional: Configure the plugin if needed\r\n }),\r\n ],\r\n})\r\n```\r\n\r\n**Why @cloudflare/vite-plugin:**\r\n- Official plugin from Cloudflare\r\n- Supports HMR with Workers\r\n- Enables local development with Miniflare\r\n- Version 1.13.13 fixes \"A hanging Promise was canceled\" error\r\n\r\n---",
"Known Issues Prevention": "This skill prevents **6 documented issues**:\r\n\r\n### Issue #1: Export Syntax Error\r\n**Error**: \"Cannot read properties of undefined (reading 'map')\"\r\n**Source**: [honojs/hono #3955](https://github.com/honojs/hono/issues/3955)\r\n**Prevention**: Use `export default app` (NOT `{ fetch: app.fetch }`)\r\n\r\n### Issue #2: Static Assets Routing Conflicts\r\n**Error**: API routes return `index.html` instead of JSON\r\n**Source**: [workers-sdk #8879](https://github.com/cloudflare/workers-sdk/issues/8879)\r\n**Prevention**: Add `\"run_worker_first\": [\"/api/*\"]` to wrangler.jsonc\r\n\r\n### Issue #3: Scheduled/Cron Not Exported\r\n**Error**: \"Handler does not export a scheduled() function\"\r\n**Source**: [honojs/vite-plugins #275](https://github.com/honojs/vite-plugins/issues/275)\r\n**Prevention**: Use Module Worker format when needed:\r\n```typescript\r\nexport default {\r\n fetch: app.fetch,\r\n scheduled: async (event, env, ctx) => { /* ... */ }\r\n}\r\n```\r\n\r\n### Issue #4: HMR Race Condition\r\n**Error**: \"A hanging Promise was canceled\" during development\r\n**Source**: [workers-sdk #9518](https://github.com/cloudflare/workers-sdk/issues/9518)\r\n**Prevention**: Use `@cloudflare/vite-plugin@1.13.13` or later\r\n\r\n### Issue #5: Static Assets Upload Race\r\n**Error**: Non-deterministic deployment failures in CI/CD\r\n**Source**: [workers-sdk #7555](https://github.com/cloudflare/workers-sdk/issues/7555)\r\n**Prevention**: Use Wrangler 4.x+ with retry logic (fixed in recent versions)\r\n\r\n### Issue #6: Service Worker Format Confusion\r\n**Error**: Using deprecated Service Worker format\r\n**Source**: Cloudflare migration guide\r\n**Prevention**: Always use ES Module format (shown in Step 1)\r\n\r\n---",
"The Four-Step Setup Process": "npm run deploy\r\n```\r\n\r\n---",
"Production Example": "This skill is based on the cloudflare-worker-base-test project:\r\n- **Live**: https://cloudflare-worker-base-test.webfonts.workers.dev\r\n- **Build Time**: ~45 minutes (actual)\r\n- **Errors**: 0 (all 6 known issues prevented)\r\n- **Validation**: ✅ Local dev, HMR, production deployment all successful\r\n\r\nAll patterns in this skill have been validated in production.\r\n\r\n---\r\n\r\n**Questions? Issues?**\r\n\r\n1. Check `reference/common-issues.md` first\r\n2. Verify all steps in the 4-step setup process\r\n3. Ensure `export default app` (not `{ fetch: app.fetch }`)\r\n4. Ensure `run_worker_first` is configured\r\n5. Check official docs: https://developers.cloudflare.com/workers/",
"Reference Documentation": "For deeper understanding, see:\r\n\r\n- **architecture.md** - Deep dive into export patterns, routing, and Static Assets\r\n- **common-issues.md** - All 6 issues with detailed troubleshooting\r\n- **deployment.md** - Wrangler commands, CI/CD patterns, and production tips\r\n\r\n---",
"Complete Setup Checklist": "- [ ] Project scaffolded with `npm create cloudflare@latest`\r\n- [ ] Dependencies installed: `hono@4.10.1`, `@cloudflare/vite-plugin@1.13.13`\r\n- [ ] `wrangler.jsonc` created with:\r\n - [ ] `account_id` set to your Cloudflare account\r\n - [ ] `assets.directory` pointing to `./public/`\r\n - [ ] `assets.run_worker_first` includes `/api/*`\r\n - [ ] `compatibility_date` set to recent date\r\n- [ ] `vite.config.ts` created with `@cloudflare/vite-plugin`\r\n- [ ] `src/index.ts` created with Hono app\r\n - [ ] Uses `export default app` (NOT `{ fetch: app.fetch }`)\r\n - [ ] Includes ASSETS binding type\r\n - [ ] Has fallback route: `app.all('*', (c) => c.env.ASSETS.fetch(c.req.raw))`\r\n- [ ] `public/` directory created with static files\r\n- [ ] `npm run cf-typegen` executed successfully\r\n- [ ] `npm run dev` starts without errors\r\n- [ ] API routes tested in browser/curl\r\n- [ ] Static assets serve correctly\r\n- [ ] HMR works without crashes\r\n- [ ] Ready to deploy with `npm run deploy`\r\n\r\n---",
"Development Workflow": "wrangler deployments list\r\n```\r\n\r\n---",
"API Route Patterns": "### Basic JSON Response\r\n\r\n```typescript\r\napp.get('/api/users', (c) => {\r\n return c.json({\r\n users: [\r\n { id: 1, name: 'Alice' },\r\n { id: 2, name: 'Bob' }\r\n ]\r\n })\r\n})\r\n```\r\n\r\n### POST with Request Body\r\n\r\n```typescript\r\napp.post('/api/users', async (c) => {\r\n const body = await c.req.json()\r\n\r\n // Validate and process body\r\n return c.json({ success: true, data: body }, 201)\r\n})\r\n```\r\n\r\n### Route Parameters\r\n\r\n```typescript\r\napp.get('/api/users/:id', (c) => {\r\n const id = c.req.param('id')\r\n return c.json({ id, name: 'User' })\r\n})\r\n```\r\n\r\n### Query Parameters\r\n\r\n```typescript\r\napp.get('/api/search', (c) => {\r\n const query = c.req.query('q')\r\n return c.json({ query, results: [] })\r\n})\r\n```\r\n\r\n### Error Handling\r\n\r\n```typescript\r\napp.get('/api/data', async (c) => {\r\n try {\r\n // Your logic here\r\n return c.json({ success: true })\r\n } catch (error) {\r\n return c.json({ error: error.message }, 500)\r\n }\r\n})\r\n```\r\n\r\n### Using Bindings (KV, D1, R2)\r\n\r\n```typescript\r\ntype Bindings = {\r\n ASSETS: Fetcher\r\n MY_KV: KVNamespace\r\n DB: D1Database\r\n MY_BUCKET: R2Bucket\r\n}\r\n\r\nconst app = new Hono<{ Bindings: Bindings }>()\r\n\r\napp.get('/api/data', async (c) => {\r\n // KV\r\n const value = await c.env.MY_KV.get('key')\r\n\r\n // D1\r\n const result = await c.env.DB.prepare('SELECT * FROM users').all()\r\n\r\n // R2\r\n const object = await c.env.MY_BUCKET.get('file.txt')\r\n\r\n return c.json({ value, result, object })\r\n})\r\n```\r\n\r\n---",
"Static Assets Best Practices": "### Directory Structure\r\n\r\n```\r\npublic/\r\n├── index.html # Main entry point\r\n├── styles.css # Global styles\r\n├── script.js # Client-side JavaScript\r\n├── favicon.ico # Favicon\r\n└── assets/ # Images, fonts, etc.\r\n ├── logo.png\r\n └── fonts/\r\n```\r\n\r\n### SPA Fallback\r\n\r\nThe `\"not_found_handling\": \"single-page-application\"` configuration means:\r\n- Unknown routes return `index.html`\r\n- Useful for React Router, Vue Router, etc.\r\n- BUT requires `run_worker_first` for API routes!\r\n\r\n### Route Priority\r\n\r\nWith `\"run_worker_first\": [\"/api/*\"]`:\r\n\r\n1. `/api/hello` → Worker handles it (returns JSON)\r\n2. `/` → Static Assets serve `index.html`\r\n3. `/styles.css` → Static Assets serve `styles.css`\r\n4. `/unknown` → Static Assets serve `index.html` (SPA fallback)\r\n\r\n### Caching Static Assets\r\n\r\nStatic Assets are automatically cached at the edge. To bust cache:\r\n```html\r\n<link rel=\"stylesheet\" href=\"/styles.css?v=1.0.0\">\r\n<script src=\"/script.js?v=1.0.0\"></script>\r\n```\r\n\r\n---",
"File Templates": "All templates are available in the `templates/` directory:\r\n\r\n- **wrangler.jsonc** - Complete Worker configuration\r\n- **vite.config.ts** - Vite + Cloudflare plugin setup\r\n- **package.json** - Dependencies and scripts\r\n- **tsconfig.json** - TypeScript configuration\r\n- **src/index.ts** - Hono app with API routes\r\n- **public/index.html** - Static frontend example\r\n- **public/styles.css** - Example styling\r\n- **public/script.js** - API test functions\r\n\r\nCopy these files to your project and customize as needed.\r\n\r\n---",
"Advanced Topics": "### Adding Middleware\r\n\r\n```typescript\r\nimport { Hono } from 'hono'\r\nimport { logger } from 'hono/logger'\r\nimport { cors } from 'hono/cors'\r\n\r\nconst app = new Hono<{ Bindings: Bindings }>()\r\n\r\n// Global middleware\r\napp.use('*', logger())\r\napp.use('/api/*', cors())\r\n\r\n// Route-specific middleware\r\napp.use('/admin/*', async (c, next) => {\r\n // Auth check\r\n await next()\r\n})\r\n```\r\n\r\n### Environment-Specific Configuration\r\n\r\n```jsonc\r\n// wrangler.jsonc\r\n{\r\n \"name\": \"my-worker\",\r\n \"env\": {\r\n \"staging\": {\r\n \"vars\": { \"ENV\": \"staging\" }\r\n },\r\n \"production\": {\r\n \"vars\": { \"ENV\": \"production\" }\r\n }\r\n }\r\n}\r\n```\r\n\r\nDeploy: `wrangler deploy --env staging`\r\n\r\n### Custom Error Pages\r\n\r\n```typescript\r\napp.onError((err, c) => {\r\n console.error(err)\r\n return c.json({ error: 'Internal Server Error' }, 500)\r\n})\r\n\r\napp.notFound((c) => {\r\n return c.json({ error: 'Not Found' }, 404)\r\n})\r\n```\r\n\r\n### Testing with Vitest\r\n\r\n```bash\r\nnpm install -D vitest @cloudflare/vitest-pool-workers\r\n```\r\n\r\nCreate `vitest.config.ts`:\r\n\r\n```typescript\r\nimport { defineConfig } from 'vitest/config'\r\n\r\nexport default defineConfig({\r\n test: {\r\n poolOptions: {\r\n workers: {\r\n wrangler: { configPath: './wrangler.jsonc' },\r\n },\r\n },\r\n },\r\n})\r\n```\r\n\r\nSee `reference/testing.md` for complete testing guide.\r\n\r\n---",
"Dependencies (Latest Verified 2025-10-20)": "```json\r\n{\r\n \"dependencies\": {\r\n \"hono\": \"^4.10.1\"\r\n },\r\n \"devDependencies\": {\r\n \"@cloudflare/vite-plugin\": \"^1.13.13\",\r\n \"@cloudflare/workers-types\": \"^4.20251011.0\",\r\n \"vite\": \"^7.0.0\",\r\n \"wrangler\": \"^4.43.0\",\r\n \"typescript\": \"^5.9.0\"\r\n }\r\n}\r\n```\r\n\r\n---",
"Official Documentation": "- **Cloudflare Workers**: https://developers.cloudflare.com/workers/\r\n- **Static Assets**: https://developers.cloudflare.com/workers/static-assets/\r\n- **Vite Plugin**: https://developers.cloudflare.com/workers/vite-plugin/\r\n- **Wrangler Configuration**: https://developers.cloudflare.com/workers/wrangler/configuration/\r\n- **Hono**: https://hono.dev/docs/getting-started/cloudflare-workers\r\n- **Context7 Library ID**: `/websites/developers_cloudflare-workers`\r\n\r\n---",
"Configuration Files Reference": "### wrangler.jsonc (Full Example)\r\n\r\n```jsonc\r\n{\r\n \"$schema\": \"node_modules/wrangler/config-schema.json\",\r\n \"name\": \"my-worker\",\r\n \"main\": \"src/index.ts\",\r\n \"account_id\": \"YOUR_ACCOUNT_ID\",\r\n \"compatibility_date\": \"2025-10-11\",\r\n \"observability\": {\r\n \"enabled\": true\r\n },\r\n \"assets\": {\r\n \"directory\": \"./public/\",\r\n \"binding\": \"ASSETS\",\r\n \"not_found_handling\": \"single-page-application\",\r\n \"run_worker_first\": [\"/api/*\"]\r\n }\r\n /* Optional: Environment Variables */\r\n // \"vars\": { \"MY_VARIABLE\": \"production_value\" }\r\n\r\n /* Optional: KV Namespace Bindings */\r\n // \"kv_namespaces\": [\r\n // { \"binding\": \"MY_KV\", \"id\": \"YOUR_KV_ID\" }\r\n // ]\r\n\r\n /* Optional: D1 Database Bindings */\r\n // \"d1_databases\": [\r\n // { \"binding\": \"DB\", \"database_name\": \"my-db\", \"database_id\": \"YOUR_DB_ID\" }\r\n // ]\r\n\r\n /* Optional: R2 Bucket Bindings */\r\n // \"r2_buckets\": [\r\n // { \"binding\": \"MY_BUCKET\", \"bucket_name\": \"my-bucket\" }\r\n // ]\r\n}\r\n```\r\n\r\n**Why wrangler.jsonc over wrangler.toml:**\r\n- JSON format preferred since Wrangler v3.91.0\r\n- Better IDE support with JSON schema\r\n- Comments allowed with JSONC\r\n\r\n### vite.config.ts (Full Example)\r\n\r\n```typescript\r\nimport { defineConfig } from 'vite'\r\nimport { cloudflare } from '@cloudflare/vite-plugin'\r\n\r\nexport default defineConfig({\r\n plugins: [\r\n cloudflare({\r\n // Persist state between HMR updates\r\n persist: true,\r\n }),\r\n ],\r\n\r\n // Optional: Configure server\r\n server: {\r\n port: 8787,\r\n },\r\n\r\n // Optional: Build optimizations\r\n build: {\r\n target: 'esnext',\r\n minify: true,\r\n },\r\n})\r\n```\r\n\r\n### tsconfig.json\r\n\r\n```json\r\n{\r\n \"compilerOptions\": {\r\n \"target\": \"ES2022\",\r\n \"module\": \"ES2022\",\r\n \"lib\": [\"ES2022\"],\r\n \"moduleResolution\": \"bundler\",\r\n \"types\": [\"@cloudflare/workers-types/2023-07-01\"],\r\n \"resolveJsonModule\": true,\r\n \"allowJs\": true,\r\n \"checkJs\": false,\r\n \"strict\": true,\r\n \"esModuleInterop\": true,\r\n \"skipLibCheck\": true,\r\n \"forceConsistentCasingInFileNames\": true,\r\n \"isolatedModules\": true,\r\n \"noEmit\": true\r\n },\r\n \"include\": [\"src/**/*\"],\r\n \"exclude\": [\"node_modules\"]\r\n}\r\n```\r\n\r\n---"
}
}---
name: cloudflare-worker-base
description: |
Production-tested setup for Cloudflare Workers with Hono, Vite, and Static Assets.
Use when: creating new Cloudflare Workers projects, setting up Hono routing with Workers,
configuring Vite plugin for Workers, adding Static Assets to Workers, deploying with Wrangler,
or encountering deployment errors, routing conflicts, or HMR crashes.
Prevents 6 documented issues: export syntax errors, Static Assets routing conflicts,
scheduled handler errors, HMR race conditions, upload race conditions, and Service Worker
format confusion.
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
```bash
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
```bash
cd my-worker
npm install hono@4.10.1
npm install -D @cloudflare/vite-plugin@1.13.13 vite@latest
```
**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`:
```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](https://github.com/cloudflare/workers-sdk/issues/8879)
### 4. Configure Vite
Create `vite.config.ts`:
```typescript
import { defineConfig } from 'vite'
import { cloudflare } from '@cloudflare/vite-plugin'
export default defineConfig({
plugins: [
cloudflare({
// Optional: Configure the plugin if needed
}),
],
})
```
**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`:
```typescript
/**
* Cloudflare Worker with Hono
*
* CRITICAL: Export pattern to prevent build errors
* ✅ CORRECT: export default app
* ❌ WRONG: export default { fetch: app.fetch }
*/
import { Hono } from 'hono'
// Type-safe environment bindings
type Bindings = {
ASSETS: Fetcher
}
const app = new Hono<{ Bindings: Bindings }>()
/**
* API Routes
* Handled BEFORE static assets due to run_worker_first config
*/
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',
})
})
/**
* Fallback to Static Assets
* Any route not matched above is served from public/ directory
*/
app.all('*', (c) => {
return c.env.ASSETS.fetch(c.req.raw)
})
/**
* Export the Hono app directly (ES Module format)
* This is the correct pattern for Cloudflare Workers with Hono + Vite
*/
export default app
```
**Why This Export Pattern:**
- Source: [honojs/hono #3955](https://github.com/honojs/hono/issues/3955)
- Using `{ fetch: app.fetch }` causes: "Cannot read properties of undefined (reading 'map')"
- Exception: If you need scheduled/tail handlers, use Module Worker format:
```typescript
export default {
fetch: app.fetch,
scheduled: async (event, env, ctx) => { /* ... */ }
}
```
### Step 2: Create Static Frontend
Create `public/index.html`:
```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`:
```javascript
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`:
```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`:
```json
{
"scripts": {
"dev": "wrangler dev",
"deploy": "wrangler deploy",
"cf-typegen": "wrangler types"
}
}
```
### Step 4: Test & Deploy
```bash
# Generate TypeScript types for bindings
npm run cf-typegen
# Start local dev server (http://localhost:8787)
npm run dev
# Deploy to production
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](https://github.com/honojs/hono/issues/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](https://github.com/cloudflare/workers-sdk/issues/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](https://github.com/honojs/vite-plugins/issues/275)
**Prevention**: Use Module Worker format when needed:
```typescript
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](https://github.com/cloudflare/workers-sdk/issues/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](https://github.com/cloudflare/workers-sdk/issues/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)
```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/*"]
}
/* Optional: Environment Variables */
// "vars": { "MY_VARIABLE": "production_value" }
/* Optional: KV Namespace Bindings */
// "kv_namespaces": [
// { "binding": "MY_KV", "id": "YOUR_KV_ID" }
// ]
/* Optional: D1 Database Bindings */
// "d1_databases": [
// { "binding": "DB", "database_name": "my-db", "database_id": "YOUR_DB_ID" }
// ]
/* Optional: R2 Bucket Bindings */
// "r2_buckets": [
// { "binding": "MY_BUCKET", "bucket_name": "my-bucket" }
// ]
}
```
**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)
```typescript
import { defineConfig } from 'vite'
import { cloudflare } from '@cloudflare/vite-plugin'
export default defineConfig({
plugins: [
cloudflare({
// Persist state between HMR updates
persist: true,
}),
],
// Optional: Configure server
server: {
port: 8787,
},
// Optional: Build optimizations
build: {
target: 'esnext',
minify: true,
},
})
```
### tsconfig.json
```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
```typescript
app.get('/api/users', (c) => {
return c.json({
users: [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
]
})
})
```
### POST with Request Body
```typescript
app.post('/api/users', async (c) => {
const body = await c.req.json()
// Validate and process body
return c.json({ success: true, data: body }, 201)
})
```
### Route Parameters
```typescript
app.get('/api/users/:id', (c) => {
const id = c.req.param('id')
return c.json({ id, name: 'User' })
})
```
### Query Parameters
```typescript
app.get('/api/search', (c) => {
const query = c.req.query('q')
return c.json({ query, results: [] })
})
```
### Error Handling
```typescript
app.get('/api/data', async (c) => {
try {
// Your logic here
return c.json({ success: true })
} catch (error) {
return c.json({ error: error.message }, 500)
}
})
```
### Using Bindings (KV, D1, R2)
```typescript
type Bindings = {
ASSETS: Fetcher
MY_KV: KVNamespace
DB: D1Database
MY_BUCKET: R2Bucket
}
const app = new Hono<{ Bindings: Bindings }>()
app.get('/api/data', async (c) => {
// KV
const value = await c.env.MY_KV.get('key')
// D1
const result = await c.env.DB.prepare('SELECT * FROM users').all()
// R2
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/*"]`:
1. `/api/hello` → Worker handles it (returns JSON)
2. `/` → Static Assets serve `index.html`
3. `/styles.css` → Static Assets serve `styles.css`
4. `/unknown` → Static Assets serve `index.html` (SPA fallback)
### Caching Static Assets
Static Assets are automatically cached at the edge. To bust cache:
```html
<link rel="stylesheet" href="/styles.css?v=1.0.0">
<script src="/script.js?v=1.0.0"></script>
```
---
## Development Workflow
### Local Development
```bash
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
```bash
# Test GET endpoint
curl http://localhost:8787/api/hello
# Test POST endpoint
curl -X POST http://localhost:8787/api/echo \
-H "Content-Type: application/json" \
-d '{"test": "data"}'
```
### Type Generation
```bash
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
```bash
# Deploy to production
npm run deploy
# Deploy to specific environment
wrangler deploy --env staging
# Tail logs in production
wrangler tail
# Check deployment status
wrangler deployments list
```
---
## Complete Setup Checklist
- [ ] Project scaffolded with `npm create cloudflare@latest`
- [ ] Dependencies installed: `hono@4.10.1`, `@cloudflare/vite-plugin@1.13.13`
- [ ] `wrangler.jsonc` created with:
- [ ] `account_id` set to your Cloudflare account
- [ ] `assets.directory` pointing to `./public/`
- [ ] `assets.run_worker_first` includes `/api/*`
- [ ] `compatibility_date` set to recent date
- [ ] `vite.config.ts` created with `@cloudflare/vite-plugin`
- [ ] `src/index.ts` created with Hono app
- [ ] Uses `export default app` (NOT `{ fetch: app.fetch }`)
- [ ] Includes ASSETS binding type
- [ ] Has fallback route: `app.all('*', (c) => c.env.ASSETS.fetch(c.req.raw))`
- [ ] `public/` directory created with static files
- [ ] `npm run cf-typegen` executed successfully
- [ ] `npm run dev` starts without errors
- [ ] API routes tested in browser/curl
- [ ] Static assets serve correctly
- [ ] HMR works without crashes
- [ ] Ready to deploy with `npm run deploy`
---
## Advanced Topics
### Adding Middleware
```typescript
import { Hono } from 'hono'
import { logger } from 'hono/logger'
import { cors } from 'hono/cors'
const app = new Hono<{ Bindings: Bindings }>()
// Global middleware
app.use('*', logger())
app.use('/api/*', cors())
// Route-specific middleware
app.use('/admin/*', async (c, next) => {
// Auth check
await next()
})
```
### Environment-Specific Configuration
```jsonc
// wrangler.jsonc
{
"name": "my-worker",
"env": {
"staging": {
"vars": { "ENV": "staging" }
},
"production": {
"vars": { "ENV": "production" }
}
}
}
```
Deploy: `wrangler deploy --env staging`
### Custom Error Pages
```typescript
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
```bash
npm install -D vitest @cloudflare/vitest-pool-workers
```
Create `vitest.config.ts`:
```typescript
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
- **Cloudflare Workers**: https://developers.cloudflare.com/workers/
- **Static Assets**: https://developers.cloudflare.com/workers/static-assets/
- **Vite Plugin**: https://developers.cloudflare.com/workers/vite-plugin/
- **Wrangler Configuration**: https://developers.cloudflare.com/workers/wrangler/configuration/
- **Hono**: https://hono.dev/docs/getting-started/cloudflare-workers
- **Context7 Library ID**: `/websites/developers_cloudflare-workers`
---
## Dependencies (Latest Verified 2025-10-20)
```json
{
"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:
- **Live**: https://cloudflare-worker-base-test.webfonts.workers.dev
- **Build Time**: ~45 minutes (actual)
- **Errors**: 0 (all 6 known issues prevented)
- **Validation**: ✅ Local dev, HMR, production deployment all successful
All patterns in this skill have been validated in production.
---
**Questions? Issues?**
1. Check `reference/common-issues.md` first
2. Verify all steps in the 4-step setup process
3. Ensure `export default app` (not `{ fetch: app.fetch }`)
4. Ensure `run_worker_first` is configured
5. Check official docs: https://developers.cloudflare.com/workers/