
Cloudflare Worker Base
- 55 installs
- 51 repo stars
- Updated November 25, 2025
- ovachiever/droid-tings
Helps with ai & agent building tasks during AI-assisted development.
About
cloudflare-worker-base is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- cloudflare-worker-base
- AI & Agent Building
- AI-coding skill
Cloudflare Worker Base by the numbers
- 55 all-time installs (skills.sh)
- Ranked #6,703 of 16,556 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Jul 27, 2026 (Skillselion catalog sync)
npx skills add https://github.com/ovachiever/droid-tings --skill cloudflare-worker-baseAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 55 |
|---|---|
| repo stars | ★ 51 |
| Last updated | November 25, 2025 |
| Repository | ovachiever/droid-tings ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
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@^7.0.0Version 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", // Find this in your Cloudflare dashboard (Workers & Pages -> Overview).
"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", // Find this in your Cloudflare dashboard (Workers & Pages -> Overview).
"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/
{
"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.",
"version": "1.0.0",
"author": {
"name": "Jeremy Dawes",
"email": "jeremy@jezweb.net"
},
"license": "MIT",
"repository": "https://github.com/jezweb/claude-skills",
"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"]
}
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.
Architecture Deep Dive
Last Updated: 2025-10-20
This document explains the architectural patterns used in Cloudflare Workers with Hono, Vite, and Static Assets.
---
Table of Contents
1. Export Patterns 2. Routing Architecture 3. Static Assets Integration 4. Bindings and Type Safety 5. Development vs Production
---
Export Patterns
The Correct Pattern (ES Module Format)
import { Hono } from 'hono'
const app = new Hono()
// Define routes...
// ✅ CORRECT: Export the Hono app directly
export default appWhy this works:
- Hono's app object already implements the
fetchhandler - When Cloudflare calls your Worker, it automatically invokes
app.fetch() - This is the ES Module Worker format (modern, recommended)
The Incorrect Pattern (Causes Errors)
// ❌ WRONG: This causes "Cannot read properties of undefined (reading 'map')" error
export default {
fetch: app.fetch
}Why this fails:
- When using Vite's build tools with Hono, the
app.fetchbinding is lost - The Vite bundler transforms the code in a way that breaks the
thiscontext - Source: honojs/hono #3955
Module Worker Format (When You Need Multiple Handlers)
import { Hono } from 'hono'
const app = new Hono()
// Define routes...
// ✅ CORRECT: Use Module Worker format for scheduled/tail handlers
export default {
fetch: app.fetch,
scheduled: async (event, env, ctx) => {
// Cron job logic
console.log('Cron triggered:', event.cron)
},
tail: async (events, env, ctx) => {
// Tail handler logic
console.log('Tail events:', events)
}
}When to use this:
- You need scheduled (cron) handlers
- You need tail handlers for log consumption
- You need queue consumers
- You need durable object handlers
Important: This is still ES Module format, not the deprecated Service Worker format.
Deprecated Service Worker Format (Never Use)
// ❌ DEPRECATED: Never use this format
addEventListener('fetch', (event) => {
event.respondWith(handleRequest(event.request))
})Why never use this:
- Deprecated since Cloudflare Workers v2
- Doesn't support modern features (D1, Vectorize, etc.)
- Not compatible with TypeScript types
- Not supported by Vite plugin
---
Routing Architecture
Request Flow
Incoming Request
↓
├─→ Worker checks run_worker_first patterns
│ └─→ Matches /api/* → Worker handles it → Returns JSON
│
└─→ No match → Static Assets handler
├─→ File exists → Returns file
└─→ File not found → SPA fallback → Returns index.htmlConfiguration Required
In wrangler.jsonc:
{
"assets": {
"directory": "./public/",
"binding": "ASSETS",
"not_found_handling": "single-page-application",
"run_worker_first": ["/api/*"]
}
}Critical: Without run_worker_first, the SPA fallback intercepts ALL requests, including API routes.
Route Priority
1. Worker routes (if matched by run_worker_first)
app.get('/api/hello', (c) => c.json({ ... }))2. Static files (if file exists in public/)
public/styles.css → Served as-is
public/logo.png → Served as-is3. SPA fallback (if file doesn't exist)
/unknown-route → Returns public/index.htmlAdvanced Routing Patterns
Wildcard Routes
// Match all API versions
app.get('/api/:version/users', (c) => {
const version = c.req.param('version')
return c.json({ version })
})
// Match nested routes
app.get('/api/users/:id/posts/:postId', (c) => {
const { id, postId } = c.req.param()
return c.json({ userId: id, postId })
})Regex Routes
// Match numeric IDs only
app.get('/api/users/:id{[0-9]+}', (c) => {
const id = c.req.param('id')
return c.json({ id: parseInt(id) })
})Route Groups
const api = new Hono()
api.get('/users', (c) => c.json({ users: [] }))
api.get('/posts', (c) => c.json({ posts: [] }))
app.route('/api', api) // Mount at /api---
Static Assets Integration
How Static Assets Work
1. Upload: When you deploy, Wrangler uploads public/ to Cloudflare's asset store 2. Binding: Your Worker receives an ASSETS Fetcher binding 3. Request: Your Worker can forward requests to ASSETS.fetch() 4. Cache: Assets are cached at the edge automatically
The Fallback Pattern
// Handle all unmatched routes
app.all('*', (c) => {
// Forward to Static Assets
return c.env.ASSETS.fetch(c.req.raw)
})What this does:
- Forwards request to Static Assets handler
- Static Assets checks if file exists
- If yes: Returns file
- If no: Returns
index.html(SPA fallback)
Custom 404 Handling
app.all('*', async (c) => {
const response = await c.env.ASSETS.fetch(c.req.raw)
// If Static Assets returns 404, customize response
if (response.status === 404) {
return c.json({ error: 'Not Found' }, 404)
}
return response
})Asset Preprocessing
app.all('*', async (c) => {
const url = new URL(c.req.url)
// Rewrite /old-path to /new-path
if (url.pathname === '/old-path') {
url.pathname = '/new-path'
}
// Create new request with modified URL
const modifiedRequest = new Request(url, c.req.raw)
return c.env.ASSETS.fetch(modifiedRequest)
})---
Bindings and Type Safety
Defining Bindings
type Bindings = {
ASSETS: Fetcher // Static Assets (always present)
MY_KV: KVNamespace // KV namespace
DB: D1Database // D1 database
MY_BUCKET: R2Bucket // R2 bucket
MY_VAR: string // Environment variable
}
const app = new Hono<{ Bindings: Bindings }>()Accessing Bindings
app.get('/api/data', async (c) => {
// Type-safe access to bindings
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')
const variable = c.env.MY_VAR
return c.json({ value, result, object, variable })
})Auto-Generated Types
Run wrangler types to generate worker-configuration.d.ts:
// Auto-generated by Wrangler
interface Env {
ASSETS: Fetcher
MY_KV: KVNamespace
DB: D1Database
MY_BUCKET: R2Bucket
MY_VAR: string
}Then use:
const app = new Hono<{ Bindings: Env }>()---
Development vs Production
Local Development (wrangler dev)
npm run devWhat happens:
- Miniflare simulates Cloudflare's runtime locally
- Bindings are emulated (KV, D1, R2)
- HMR enabled via Vite plugin
- Runs on http://localhost:8787
Configuration:
// vite.config.ts
export default defineConfig({
plugins: [
cloudflare({
persist: true, // Persist data between restarts
}),
],
})Production Deployment (wrangler deploy)
npm run deployWhat happens:
- Vite builds your code
- Wrangler uploads to Cloudflare
- Static Assets uploaded separately
- Worker deployed to edge network
Build Output:
dist/
├── index.js # Your Worker code (bundled)
└── ... # Other build artifactsEnvironment-Specific Configuration
// wrangler.jsonc
{
"name": "my-worker",
"env": {
"staging": {
"name": "my-worker-staging",
"vars": { "ENV": "staging" },
"kv_namespaces": [
{ "binding": "MY_KV", "id": "staging-kv-id" }
]
},
"production": {
"name": "my-worker-production",
"vars": { "ENV": "production" },
"kv_namespaces": [
{ "binding": "MY_KV", "id": "production-kv-id" }
]
}
}
}Deploy to specific environment:
wrangler deploy --env staging
wrangler deploy --env productionEnvironment Detection in Code
app.get('/api/info', (c) => {
const isDev = c.req.url.includes('localhost')
const env = c.env.ENV || 'development'
return c.json({ isDev, env })
})---
Performance Considerations
Cold Starts
Cloudflare Workers have extremely fast cold starts (~5ms):
- Code is distributed globally
- No containers to spin up
- Minimal initialization overhead
Keep your bundle small:
- Avoid large dependencies
- Use tree-shaking (Vite does this automatically)
- Lazy-load heavy modules
CPU Time Limits
- Free Plan: 10ms CPU time per request
- Paid Plan: 50ms CPU time per request
Tip: Use asynchronous operations (KV, D1, R2) to avoid blocking CPU time.
Memory Limits
- 128 MB per Worker instance
Tip: Avoid loading large files into memory. Stream data when possible.
Request Size Limits
- Request Body: 100 MB
- Response Body: No limit (can stream)
---
Best Practices
1. Use Middleware for Common Logic
import { logger } from 'hono/logger'
import { cors } from 'hono/cors'
app.use('*', logger())
app.use('/api/*', cors())2. Separate API and Static Routes
const api = new Hono()
api.get('/users', ...)
api.get('/posts', ...)
app.route('/api', api)
app.all('*', (c) => c.env.ASSETS.fetch(c.req.raw))3. Handle Errors Gracefully
app.onError((err, c) => {
console.error(err)
return c.json({ error: 'Internal Server Error' }, 500)
})4. Use TypeScript
// Define types for request/response
type User = {
id: number
name: string
}
app.get('/api/users/:id', async (c) => {
const id = parseInt(c.req.param('id'))
const user: User = { id, name: 'Alice' }
return c.json(user)
})5. Validate Input
import { z } from 'zod'
const schema = z.object({
name: z.string(),
email: z.string().email(),
})
app.post('/api/users', async (c) => {
const body = await c.req.json()
const validated = schema.parse(body)
return c.json({ success: true, data: validated })
})---
Troubleshooting
Issue: API routes return HTML
Cause: Missing run_worker_first configuration
Fix: Add to wrangler.jsonc:
{
"assets": {
"run_worker_first": ["/api/*"]
}
}Issue: HMR crashes with "A hanging Promise was canceled"
Cause: Race condition in older Vite plugin versions
Fix: Update to latest:
npm install -D @cloudflare/vite-plugin@1.13.13Issue: Deployment fails with "Cannot read properties of undefined"
Cause: Incorrect export pattern
Fix: Use export default app (not { fetch: app.fetch })
---
For more troubleshooting, see common-issues.md.
Common Issues and Troubleshooting
Last Updated: 2025-10-20
This document details all 6 documented issues that commonly affect Cloudflare Workers projects, with detailed explanations and fixes.
---
Table of Contents
1. Issue #1: Export Syntax Error 2. Issue #2: Static Assets Routing Conflicts 3. Issue #3: Scheduled/Cron Not Exported 4. Issue #4: HMR Race Condition 5. Issue #5: Static Assets Upload Race 6. Issue #6: Service Worker Format Confusion
---
Issue #1: Export Syntax Error
Symptoms
Error: Cannot read properties of undefined (reading 'map')Deployment fails with TypeError during build or runtime.
Source
- GitHub Issue: honojs/hono #3955
- Related: honojs/vite-plugins #237
- Reported: February 2025
Root Cause
When using Hono with Vite's build tools, the incorrect export pattern breaks the this context:
// ❌ WRONG: This causes the error
export default {
fetch: app.fetch
}Why it breaks:
- Vite's bundler transforms the code
- The
app.fetchbinding loses itsthiscontext - When Cloudflare calls
fetch(),thisisundefined - Hono tries to access
this.routes.map(...)→ Error
Fix
Use the direct export pattern:
import { Hono } from 'hono'
const app = new Hono()
// Define routes...
// ✅ CORRECT
export default appWhy this works:
- Hono's app object already implements the fetch handler
- No context binding is lost
- Vite can properly bundle the code
Exception: When You Need Multiple Handlers
If you need scheduled/tail handlers, use Module Worker format:
export default {
fetch: app.fetch,
scheduled: async (event, env, ctx) => {
console.log('Cron triggered:', event.cron)
}
}This works because Cloudflare's runtime handles the binding correctly for Module Workers.
How to Verify Fix
1. Check your src/index.ts export 2. Ensure it's export default app 3. Run npm run dev → Should start without errors 4. Run npm run deploy → Should deploy successfully 5. Test API endpoints → Should return JSON (not errors)
---
Issue #2: Static Assets Routing Conflicts
Symptoms
- API routes return
index.htmlinstead of JSON - API endpoints return status 200 but wrong content-type (text/html instead of application/json)
- Browser console shows HTML when expecting JSON
Example
curl http://localhost:8787/api/hello
# Expected: {"message":"Hello"}
# Actual: <!DOCTYPE html><html>...Source
- GitHub Issue: workers-sdk #8879
- Reported: April 2025
Root Cause
The not_found_handling: "single-page-application" configuration creates a fallback:
Request → File not found → Return index.htmlWithout `run_worker_first`: 1. Request to /api/hello 2. Static Assets handler checks: "Does /api/hello file exist?" 3. No → SPA fallback → Returns public/index.html 4. Your Worker never runs!
Fix
Add run_worker_first to wrangler.jsonc:
{
"assets": {
"directory": "./public/",
"binding": "ASSETS",
"not_found_handling": "single-page-application",
"run_worker_first": ["/api/*"] // ← CRITICAL
}
}What this does:
- Requests matching
/api/*go to your Worker FIRST - If Worker doesn't handle it, then try Static Assets
- Ensures API routes are never intercepted by SPA fallback
Advanced Configuration
{
"assets": {
"run_worker_first": [
"/api/*",
"/auth/*",
"/webhooks/*",
"/_app/*"
]
}
}How to Verify Fix
1. Start dev server: npm run dev 2. Test API endpoint:
curl -i http://localhost:8787/api/hello3. Check response:
- ✅
Content-Type: application/json - ✅ JSON body
4. Test static file:
curl -i http://localhost:8787/5. Check response:
- ✅
Content-Type: text/html - ✅ HTML body
---
Issue #3: Scheduled/Cron Not Exported
Symptoms
Error: Handler does not export a scheduled() functionDeployment succeeds, but cron triggers fail.
Source
- GitHub Issue: honojs/vite-plugins #275
- Reported: July 2025
Root Cause
The @hono/vite-build/cloudflare-workers plugin only supports the `fetch` handler.
If you use:
export default app // Only exports fetch handler...then scheduled/tail handlers are not exported.
Fix Option 1: Use Module Worker Format
import { Hono } from 'hono'
const app = new Hono()
// Define routes...
// ✅ Export multiple handlers
export default {
fetch: app.fetch,
scheduled: async (event, env, ctx) => {
console.log('Cron triggered:', event.cron)
// Your scheduled logic here
},
tail: async (events, env, ctx) => {
// Tail handler logic
console.log('Tail events:', events)
}
}Fix Option 2: Use @cloudflare/vite-plugin
Instead of @hono/vite-build/cloudflare-workers, use the official Cloudflare plugin:
npm uninstall @hono/vite-build
npm install -D @cloudflare/vite-pluginUpdate vite.config.ts:
import { defineConfig } from 'vite'
import { cloudflare } from '@cloudflare/vite-plugin'
export default defineConfig({
plugins: [cloudflare()],
})This plugin supports all handler types.
Configure Cron in wrangler.jsonc
{
"triggers": {
"crons": ["0 0 * * *"] // Daily at midnight UTC
}
}How to Verify Fix
1. Deploy: npm run deploy 2. Trigger manually:
wrangler deploy && wrangler tail3. Wait for cron or trigger via dashboard 4. Check logs for scheduled handler output
---
Issue #4: HMR Race Condition
Symptoms
Error: A hanging Promise was canceled- Development server crashes during file changes
- Happens with rapid HMR updates
- Requires manual restart
Source
- GitHub Issue: workers-sdk #9518
- Related: workers-sdk #9249
- Reported: June 2025
Root Cause
Race condition in `@cloudflare/vite-plugin` versions 1.1.1 through 1.11.x:
1. File change detected 2. Vite triggers HMR 3. Plugin cancels old Worker instance 4. New instance starts before old one fully terminates 5. Promise cancellation error thrown
Fix
Update to latest @cloudflare/vite-plugin:
npm install -D @cloudflare/vite-plugin@1.13.13Fixed in version 1.13.13 (October 2025)
Alternative: Configure Vite with Persistence
If updating doesn't fix it, try:
// vite.config.ts
import { defineConfig } from 'vite'
import { cloudflare } from '@cloudflare/vite-plugin'
export default defineConfig({
plugins: [
cloudflare({
persist: true, // Persist state between HMR updates
}),
],
})How to Verify Fix
1. Start dev server: npm run dev 2. Make rapid file changes (edit src/index.ts 5 times quickly) 3. Check terminal:
- ✅ No "hanging Promise" errors
- ✅ HMR updates smoothly
4. Test API endpoint after each change:
curl http://localhost:8787/api/hello---
Issue #5: Static Assets Upload Race
Symptoms
- Deployment fails non-deterministically in CI/CD
- Works locally, fails in CI randomly
- Error messages vary:
- "Failed to upload assets"
- "Timeout during asset upload"
- "Asset manifest mismatch"
Source
- GitHub Issue: workers-sdk #7555
- Reported: March 2025
Root Cause
Race condition during parallel asset uploads:
1. Wrangler uploads multiple assets simultaneously 2. Cloudflare's asset store processes uploads 3. Manifest is generated before all uploads complete 4. Deployment validation fails
Most common in CI/CD because:
- Network latency varies
- Parallel execution timing is different
- No user interaction to retry
Fix Option 1: Use Wrangler 4.x+ (Recommended)
Wrangler 4.x includes improved upload logic:
npm install -D wrangler@latestImprovements in 4.x:
- Sequential upload of critical assets
- Better retry logic
- Manifest generation after all uploads complete
Fix Option 2: Add Retry Logic to CI/CD
# GitHub Actions example
- name: Deploy to Cloudflare
run: |
for i in {1..3}; do
npm run deploy && break || sleep 10
done# Shell script
#!/bin/bash
for i in {1..3}; do
npm run deploy && break || sleep 10
doneFix Option 3: Reduce Asset Count
If you have many small files, bundle them:
// vite.config.ts
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
},
},
},
},
})How to Verify Fix
1. Deploy locally 5 times:
for i in {1..5}; do npm run deploy; done2. All deployments should succeed 3. Run in CI/CD pipeline 4. Check logs for upload errors
---
Issue #6: Service Worker Format Confusion
Symptoms
- Using deprecated
addEventListener('fetch', ...)pattern - TypeScript errors about missing types
- Bindings don't work (KV, D1, R2)
- Modern Cloudflare features unavailable
Source
- Cloudflare Migration Guide: https://developers.cloudflare.com/workers/configuration/compatibility-dates/
- Multiple Stack Overflow questions (2024-2025)
Root Cause
Old tutorials and templates still use the deprecated Service Worker format:
// ❌ DEPRECATED: Service Worker format
addEventListener('fetch', (event) => {
event.respondWith(handleRequest(event.request))
})
async function handleRequest(request) {
return new Response('Hello World')
}Problems with this format:
- Doesn't support bindings (KV, D1, R2, etc.)
- No TypeScript types
- No environment variable access
- Deprecated since Workers v2 (2021)
Fix: Use ES Module Format
// ✅ CORRECT: ES Module format
export default {
fetch(request, env, ctx) {
return new Response('Hello World')
}
}With Hono:
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => c.text('Hello World'))
export default appMigration Steps
1. Remove `addEventListener`:
- addEventListener('fetch', (event) => {
- event.respondWith(handleRequest(event.request))
- })2. Change to ES Module export:
+ export default {
+ fetch(request, env, ctx) {
+ return handleRequest(request, env)
+ }
+ }3. Update function signatures to accept env:
- async function handleRequest(request) {
+ async function handleRequest(request, env) {
// Now you can access env.MY_KV, env.DB, etc.4. Update `wrangler.toml` → `wrangler.jsonc`:
# Convert TOML to JSONC (preferred since Wrangler v3.91.0)How to Verify Fix
1. Check src/index.ts:
- ✅ No
addEventListener - ✅ Has
export default
2. Check you can access bindings:
const value = await env.MY_KV.get('key')3. TypeScript types work:
type Bindings = {
MY_KV: KVNamespace
}---
General Troubleshooting Tips
Check Package Versions
npm list hono @cloudflare/vite-plugin wranglerExpected (as of 2025-10-20):
hono@4.10.1@cloudflare/vite-plugin@1.13.13wrangler@4.43.0
Clear Wrangler Cache
rm -rf node_modules/.wrangler
rm -rf .wrangler
npm run devCheck Wrangler Config
wrangler whoami # Verify authentication
wrangler dev --local # Test without deployingEnable Verbose Logging
WRANGLER_LOG=debug npm run dev
WRANGLER_LOG=debug npm run deployCheck Browser Console
Many issues are visible in the browser:
- Open DevTools → Network tab
- Check response Content-Type
- Check response body
- Look for CORS errors
Test with curl
# Test API endpoint
curl -i http://localhost:8787/api/hello
# Test POST
curl -X POST http://localhost:8787/api/echo \
-H "Content-Type: application/json" \
-d '{"test":"data"}'
# Test static file
curl -i http://localhost:8787/styles.css---
Issue Summary Table
| Issue | Error Message | Source | Fix |
|---|---|---|---|
| #1 | "Cannot read properties of undefined" | hono #3955 | export default app |
| #2 | API routes return HTML | workers-sdk #8879 | run_worker_first: ["/api/*"] |
| #3 | "Handler does not export scheduled()" | vite-plugins #275 | Module Worker format or @cloudflare/vite-plugin |
| #4 | "A hanging Promise was canceled" | workers-sdk #9518 | Update to vite-plugin@1.13.13+ |
| #5 | Non-deterministic deployment failures | workers-sdk #7555 | Use Wrangler 4.x+ with retry |
| #6 | Service Worker format issues | Cloudflare migration | Use ES Module format |
---
Getting Help
If you encounter issues not covered here:
1. Check official docs:
- Cloudflare Workers: https://developers.cloudflare.com/workers/
- Hono: https://hono.dev/
2. Search GitHub issues:
- workers-sdk: https://github.com/cloudflare/workers-sdk/issues
- hono: https://github.com/honojs/hono/issues
3. Ask in Discord:
- Cloudflare Developers: https://discord.gg/cloudflaredev
- Hono: https://discord.gg/hono
4. Check Stack Overflow:
- Tag:
cloudflare-workers
---
All issues documented with GitHub sources ✅ All fixes production-tested ✅
Deployment Guide
Last Updated: 2025-10-20
Complete guide to deploying Cloudflare Workers with Wrangler, including CI/CD patterns and production best practices.
---
Table of Contents
1. Prerequisites 2. Wrangler Commands 3. Environment Configuration 4. CI/CD Pipelines 5. Production Best Practices 6. Monitoring and Logs
---
Prerequisites
1. Cloudflare Account
Sign up at https://dash.cloudflare.com/sign-up
2. Get Account ID
# Option 1: From dashboard
# Go to: Workers & Pages → Overview → Account ID (right sidebar)
# Option 2: Via Wrangler
wrangler whoamiAdd to wrangler.jsonc:
{
"account_id": "YOUR_ACCOUNT_ID_HERE"
}3. Authenticate Wrangler
# Login via browser
wrangler login
# Or use API token (for CI/CD)
export CLOUDFLARE_API_TOKEN="your-token"Create API token: 1. Go to: https://dash.cloudflare.com/profile/api-tokens 2. Click "Create Token" 3. Use template: "Edit Cloudflare Workers" 4. Copy token (only shown once!)
---
Wrangler Commands
Development
# Start local dev server (http://localhost:8787)
wrangler dev
# Local mode (no network requests to Cloudflare)
wrangler dev --local
# Custom port
wrangler dev --port 3000
# Specific environment
wrangler dev --env stagingDeployment
# Deploy to production
wrangler deploy
# Deploy to specific environment
wrangler deploy --env staging
wrangler deploy --env production
# Dry run (validate without deploying)
wrangler deploy --dry-run
# Deploy with compatibility date
wrangler deploy --compatibility-date 2025-10-11Type Generation
# Generate TypeScript types for bindings
wrangler types
# Output to custom file
wrangler types --output-file=types/worker.d.tsLogs
# Tail live logs
wrangler tail
# Filter by status code
wrangler tail --status error
# Filter by HTTP method
wrangler tail --method POST
# Filter by IP
wrangler tail --ip-address 1.2.3.4
# Format as JSON
wrangler tail --format jsonDeployments
# List recent deployments
wrangler deployments list
# View specific deployment
wrangler deployments view DEPLOYMENT_ID
# Rollback to previous deployment
wrangler rollback --deployment-id DEPLOYMENT_IDSecrets
# Set secret (interactive)
wrangler secret put MY_SECRET
# Set secret from file
echo "secret-value" | wrangler secret put MY_SECRET
# List secrets
wrangler secret list
# Delete secret
wrangler secret delete MY_SECRETKV Operations
# Create KV namespace
wrangler kv namespace create MY_KV
# List namespaces
wrangler kv namespace list
# Put key-value
wrangler kv key put --namespace-id=YOUR_ID "key" "value"
# Get value
wrangler kv key get --namespace-id=YOUR_ID "key"
# List keys
wrangler kv key list --namespace-id=YOUR_IDD1 Operations
# Create D1 database
wrangler d1 create my-database
# Execute SQL
wrangler d1 execute my-database --command "SELECT * FROM users"
# Run SQL file
wrangler d1 execute my-database --file schema.sql
# List databases
wrangler d1 listR2 Operations
# Create R2 bucket
wrangler r2 bucket create my-bucket
# List buckets
wrangler r2 bucket list
# Upload file
wrangler r2 object put my-bucket/file.txt --file local-file.txt
# Download file
wrangler r2 object get my-bucket/file.txt --file local-file.txt---
Environment Configuration
Single Environment (Default)
{
"name": "my-worker",
"account_id": "YOUR_ACCOUNT_ID",
"main": "src/index.ts",
"compatibility_date": "2025-10-11",
"vars": {
"ENV": "production"
},
"kv_namespaces": [
{ "binding": "MY_KV", "id": "production-kv-id" }
]
}Multiple Environments
{
"name": "my-worker",
"account_id": "YOUR_ACCOUNT_ID",
"main": "src/index.ts",
"compatibility_date": "2025-10-11",
// Shared configuration
"observability": {
"enabled": true
},
// Environment-specific configuration
"env": {
"staging": {
"name": "my-worker-staging",
"vars": {
"ENV": "staging",
"API_URL": "https://api-staging.example.com"
},
"kv_namespaces": [
{ "binding": "MY_KV", "id": "staging-kv-id" }
],
"d1_databases": [
{ "binding": "DB", "database_name": "my-db-staging", "database_id": "staging-db-id" }
]
},
"production": {
"name": "my-worker-production",
"vars": {
"ENV": "production",
"API_URL": "https://api.example.com"
},
"kv_namespaces": [
{ "binding": "MY_KV", "id": "production-kv-id" }
],
"d1_databases": [
{ "binding": "DB", "database_name": "my-db", "database_id": "production-db-id" }
],
"routes": [
{ "pattern": "example.com/*", "zone_name": "example.com" }
]
}
}
}Deploy:
wrangler deploy --env staging
wrangler deploy --env productionEnvironment Detection in Code
app.get('/api/info', (c) => {
const env = c.env.ENV || 'development'
const apiUrl = c.env.API_URL || 'http://localhost:3000'
return c.json({ env, apiUrl })
})---
CI/CD Pipelines
GitHub Actions
Create .github/workflows/deploy.yml:
name: Deploy to Cloudflare Workers
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
name: Deploy
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Deploy to Cloudflare Workers
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}With environment-specific deployment:
name: Deploy
on:
push:
branches:
- main
- staging
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Deploy to staging
if: github.ref == 'refs/heads/staging'
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: deploy --env staging
- name: Deploy to production
if: github.ref == 'refs/heads/main'
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: deploy --env productionAdd secrets to GitHub: 1. Go to: Repository → Settings → Secrets → Actions 2. Add CLOUDFLARE_API_TOKEN 3. Add CLOUDFLARE_ACCOUNT_ID
GitLab CI/CD
Create .gitlab-ci.yml:
image: node:20
stages:
- test
- deploy
test:
stage: test
script:
- npm ci
- npm test
deploy_staging:
stage: deploy
script:
- npm ci
- npx wrangler deploy --env staging
only:
- staging
environment:
name: staging
deploy_production:
stage: deploy
script:
- npm ci
- npx wrangler deploy --env production
only:
- main
environment:
name: productionAdd variables to GitLab: 1. Go to: Settings → CI/CD → Variables 2. Add CLOUDFLARE_API_TOKEN (masked) 3. Add CLOUDFLARE_ACCOUNT_ID
Manual Deployment Script
Create scripts/deploy.sh:
#!/bin/bash
set -e
ENV=${1:-production}
echo "🚀 Deploying to $ENV..."
# Run tests
echo "Running tests..."
npm test
# Type check
echo "Type checking..."
npm run type-check
# Build
echo "Building..."
npm run build
# Deploy
echo "Deploying to Cloudflare..."
if [ "$ENV" = "production" ]; then
wrangler deploy --env production
else
wrangler deploy --env staging
fi
echo "✅ Deployment complete!"
echo "🔗 Check logs: wrangler tail --env $ENV"Usage:
chmod +x scripts/deploy.sh
./scripts/deploy.sh staging
./scripts/deploy.sh production---
Production Best Practices
1. Use Compatibility Dates
Always set a recent compatibility_date:
{
"compatibility_date": "2025-10-11"
}Why: Ensures consistent behavior and access to new features.
Update regularly: Check https://developers.cloudflare.com/workers/configuration/compatibility-dates/
2. Enable Observability
{
"observability": {
"enabled": true
}
}Provides:
- Real-time metrics
- Error tracking
- Performance monitoring
3. Set Resource Limits
{
"limits": {
"cpu_ms": 50 // Maximum CPU time per request (paid plan)
}
}4. Configure Custom Domains
{
"routes": [
{
"pattern": "api.example.com/*",
"zone_name": "example.com"
}
]
}Or via dashboard: 1. Workers & Pages → Your Worker → Triggers 2. Add Custom Domain
5. Use Secrets for Sensitive Data
# Never commit secrets to git
wrangler secret put API_KEY
wrangler secret put DATABASE_URL// Access in code
const apiKey = c.env.API_KEY6. Implement Rate Limiting
import { Hono } from 'hono'
const app = new Hono()
app.use('/api/*', async (c, next) => {
const ip = c.req.header('cf-connecting-ip')
const key = `rate-limit:${ip}`
const count = await c.env.MY_KV.get(key)
if (count && parseInt(count) > 100) {
return c.json({ error: 'Rate limit exceeded' }, 429)
}
await c.env.MY_KV.put(key, (parseInt(count || '0') + 1).toString(), {
expirationTtl: 60 // 1 minute
})
await next()
})7. Add Health Check Endpoint
app.get('/health', (c) => {
return c.json({
status: 'ok',
version: '1.0.0',
timestamp: new Date().toISOString()
})
})8. Implement Error Tracking
app.onError((err, c) => {
console.error('Error:', err)
// Send to error tracking service
// await sendToSentry(err)
return c.json({
error: 'Internal Server Error',
requestId: c.req.header('cf-ray')
}, 500)
})9. Use Structured Logging
import { logger } from 'hono/logger'
app.use('*', logger())
app.get('/api/users', (c) => {
console.log(JSON.stringify({
level: 'info',
message: 'Fetching users',
userId: c.req.header('x-user-id'),
timestamp: new Date().toISOString()
}))
return c.json({ users: [] })
})10. Test Before Deploying
# Run tests
npm test
# Type check
npm run type-check
# Lint
npm run lint
# Test locally
wrangler dev --local
# Test remotely (without deploying)
wrangler dev---
Monitoring and Logs
Real-Time Logs
# Tail all requests
wrangler tail
# Filter by status
wrangler tail --status error
wrangler tail --status ok
# Filter by method
wrangler tail --method POST
# Filter by search term
wrangler tail --search "error"
# Output as JSON
wrangler tail --format jsonAnalytics Dashboard
View in Cloudflare Dashboard: 1. Workers & Pages → Your Worker → Metrics 2. See:
- Requests per second
- Errors
- CPU time
- Response time
Custom Metrics
app.use('*', async (c, next) => {
const start = Date.now()
await next()
const duration = Date.now() - start
console.log(JSON.stringify({
type: 'metric',
name: 'request_duration',
value: duration,
path: c.req.path,
method: c.req.method,
status: c.res.status
}))
})External Monitoring
Use Workers Analytics Engine:
app.use('*', async (c, next) => {
await next()
// Write to Analytics Engine
c.env.ANALYTICS.writeDataPoint({
indexes: [c.req.path],
blobs: [c.req.method, c.req.header('user-agent')],
doubles: [Date.now(), c.res.status]
})
})Or send to external services:
// Send to Datadog, New Relic, etc.
await fetch('https://api.datadoghq.com/api/v1/logs', {
method: 'POST',
headers: {
'DD-API-KEY': c.env.DATADOG_API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({
message: 'Request processed',
status: c.res.status,
path: c.req.path
})
})---
Rollback Strategy
Immediate Rollback
# List recent deployments
wrangler deployments list
# Rollback to specific deployment
wrangler rollback --deployment-id DEPLOYMENT_IDGradual Rollout
{
"name": "my-worker-canary",
"routes": [
{
"pattern": "example.com/*",
"zone_name": "example.com",
"script": "my-worker"
}
]
}1. Deploy new version to -canary worker 2. Route 10% of traffic to canary 3. Monitor metrics 4. Gradually increase to 100% 5. Promote canary to main
---
Performance Optimization
1. Minimize Bundle Size
# Check bundle size
wrangler deploy --dry-run --outdir=dist
# Analyze
ls -lh dist/Tips:
- Avoid large dependencies
- Use dynamic imports for heavy modules
- Tree-shake unused code
2. Use Edge Caching
app.get('/api/data', async (c) => {
const cache = caches.default
const cacheKey = new Request(c.req.url, c.req.raw)
let response = await cache.match(cacheKey)
if (!response) {
// Fetch data
const data = await fetchData()
response = c.json(data)
// Cache for 5 minutes
response.headers.set('Cache-Control', 'max-age=300')
c.executionCtx.waitUntil(cache.put(cacheKey, response.clone()))
}
return response
})3. Optimize Database Queries
// ❌ Bad: N+1 queries
for (const user of users) {
const posts = await c.env.DB.prepare('SELECT * FROM posts WHERE user_id = ?').bind(user.id).all()
}
// ✅ Good: Single query
const posts = await c.env.DB.prepare('SELECT * FROM posts WHERE user_id IN (?)').bind(userIds).all()---
Troubleshooting Deployments
Deployment Fails
# Check configuration
wrangler deploy --dry-run
# Verbose output
WRANGLER_LOG=debug wrangler deploy
# Check account access
wrangler whoamiBuild Errors
# Clear cache
rm -rf node_modules/.wrangler
rm -rf .wrangler
# Reinstall dependencies
npm ci
# Try again
npm run deployRoutes Not Working
# List routes
wrangler routes list
# Check zone assignment
# Dashboard → Workers & Pages → Your Worker → Triggers---
Production-tested deployment patterns ✅ CI/CD examples validated ✅ Monitoring strategies proven ✅
{
"name": "cloudflare-worker-base-test",
"version": "0.0.0",
"private": true,
"scripts": {
"deploy": "wrangler deploy",
"dev": "wrangler dev",
"start": "wrangler dev",
"test": "vitest",
"cf-typegen": "wrangler types"
},
"devDependencies": {
"@cloudflare/vite-plugin": "^1.13.13",
"@cloudflare/vitest-pool-workers": "^0.8.19",
"typescript": "^5.5.2",
"vite": "^7.1.10",
"vitest": "~3.2.0",
"wrangler": "^4.43.0"
},
"dependencies": {
"hono": "^4.10.1"
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cloudflare Worker + Hono + Static Assets</title>
<link rel="stylesheet" href="/styles.css">
</head>
<body>
<div class="container">
<header>
<h1>🔥 Cloudflare Worker + Hono + Static Assets</h1>
<p>Testing API routes with Workers Static Assets and SPA fallback</p>
</header>
<section class="test-section">
<h2>API Tests</h2>
<p>These API routes are handled by the Worker thanks to <code>run_worker_first</code> configuration.</p>
<div class="test-group">
<button onclick="testHello()">Test /api/hello</button>
<button onclick="testData()">Test /api/data</button>
<button onclick="testEcho()">Test /api/echo (POST)</button>
<button onclick="testHealth()">Test /api/health</button>
</div>
<div id="results">
<h3>Results:</h3>
<pre id="output">Click a button to test the API...</pre>
</div>
</section>
<section class="info-section">
<h2>✅ What This Demonstrates</h2>
<ul>
<li><strong>Static Assets</strong>: This HTML is served from <code>public/</code></li>
<li><strong>API Routes</strong>: <code>/api/*</code> routes are handled by Worker first</li>
<li><strong>SPA Fallback</strong>: Unknown routes return this index.html</li>
<li><strong>Hono Framework</strong>: Type-safe routing with JSON responses</li>
<li><strong>ES Module Format</strong>: Correct export pattern prevents build errors</li>
</ul>
</section>
<footer>
<p>
📚 <a href="https://developers.cloudflare.com/workers/static-assets/">Static Assets Docs</a> |
<a href="https://hono.dev/docs/getting-started/cloudflare-workers">Hono Docs</a> |
<a href="https://developers.cloudflare.com/workers/vite-plugin/">Vite Plugin Docs</a>
</p>
</footer>
</div>
<script src="/script.js"></script>
</body>
</html>
/**
* API Test Functions
*
* These functions call the Worker API routes and display the results.
* Notice how API routes work seamlessly with static assets thanks to
* the "run_worker_first" configuration in wrangler.jsonc
*/
const output = document.getElementById('output')
function displayResult(data, status = 200) {
const formatted = JSON.stringify(data, null, 2)
output.textContent = `Status: ${status}\n\n${formatted}`
output.style.borderLeft = status === 200 ? '4px solid #4caf50' : '4px solid #f44336'
}
function displayError(error) {
output.textContent = `Error: ${error.message}\n\nCheck console for details.`
output.style.borderLeft = '4px solid #f44336'
console.error('API Error:', error)
}
async function testHello() {
try {
const response = await fetch('/api/hello')
const data = await response.json()
displayResult(data, response.status)
} catch (error) {
displayError(error)
}
}
async function testData() {
try {
const response = await fetch('/api/data')
const data = await response.json()
displayResult(data, response.status)
} catch (error) {
displayError(error)
}
}
async function testEcho() {
try {
const payload = {
test: 'data',
timestamp: new Date().toISOString(),
random: Math.random(),
}
const response = await fetch('/api/echo', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
})
const data = await response.json()
displayResult(data, response.status)
} catch (error) {
displayError(error)
}
}
async function testHealth() {
try {
const response = await fetch('/api/health')
const data = await response.json()
displayResult(data, response.status)
} catch (error) {
displayError(error)
}
}
// Display welcome message on load
window.addEventListener('DOMContentLoaded', () => {
displayResult({
message: 'Welcome! Click a button above to test the API.',
info: 'All API routes are handled by the Cloudflare Worker',
static_assets: 'This HTML/CSS/JS is served from public/ directory',
})
})
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
line-height: 1.6;
color: #333;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 2rem;
}
.container {
max-width: 900px;
margin: 0 auto;
background: white;
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
overflow: hidden;
}
header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 3rem 2rem;
text-align: center;
}
header h1 {
font-size: 2rem;
margin-bottom: 0.5rem;
}
header p {
opacity: 0.9;
font-size: 1.1rem;
}
section {
padding: 2rem;
}
h2 {
color: #667eea;
margin-bottom: 1rem;
font-size: 1.5rem;
}
h3 {
color: #555;
margin-bottom: 0.5rem;
font-size: 1.2rem;
}
.test-section {
border-bottom: 1px solid #e0e0e0;
}
.test-group {
display: flex;
flex-wrap: wrap;
gap: 1rem;
margin-bottom: 1.5rem;
}
button {
background: #667eea;
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 6px;
font-size: 1rem;
cursor: pointer;
transition: all 0.2s;
font-weight: 500;
}
button:hover {
background: #5568d3;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
button:active {
transform: translateY(0);
}
#results {
background: #f5f5f5;
border-radius: 8px;
padding: 1.5rem;
}
#output {
background: #1e1e1e;
color: #d4d4d4;
padding: 1rem;
border-radius: 6px;
overflow-x: auto;
font-family: 'Monaco', 'Courier New', monospace;
font-size: 0.9rem;
line-height: 1.5;
white-space: pre-wrap;
word-wrap: break-word;
}
.info-section ul {
list-style: none;
padding-left: 0;
}
.info-section li {
padding: 0.75rem;
margin-bottom: 0.5rem;
background: #f8f9fa;
border-left: 4px solid #667eea;
border-radius: 4px;
}
.info-section strong {
color: #667eea;
}
code {
background: #e8eaf6;
padding: 0.2rem 0.4rem;
border-radius: 3px;
font-family: 'Monaco', 'Courier New', monospace;
font-size: 0.9em;
color: #5e35b1;
}
footer {
background: #f8f9fa;
padding: 1.5rem 2rem;
text-align: center;
border-top: 1px solid #e0e0e0;
}
footer a {
color: #667eea;
text-decoration: none;
font-weight: 500;
margin: 0 0.5rem;
}
footer a:hover {
text-decoration: underline;
}
@media (max-width: 600px) {
body {
padding: 1rem;
}
header {
padding: 2rem 1rem;
}
header h1 {
font-size: 1.5rem;
}
section {
padding: 1.5rem;
}
.test-group {
flex-direction: column;
}
button {
width: 100%;
}
}
/**
* Cloudflare Worker with Hono
*
* CRITICAL: Export pattern to prevent "Cannot read properties of undefined (reading 'map')" error
* See: https://github.com/honojs/hono/issues/3955
*
* ✅ CORRECT: export default app (for Hono apps)
* ❌ WRONG: export default { fetch: app.fetch } (causes build errors with Vite)
*
* Exception: If you need multiple handlers (scheduled, tail, etc.), use Module Worker format:
* export default {
* fetch: app.fetch,
* scheduled: async (event, env, ctx) => { ... }
* }
*/
import { Hono } from 'hono'
// Type-safe environment bindings
type Bindings = {
ASSETS: Fetcher
}
const app = new Hono<{ Bindings: Bindings }>()
/**
* API Routes
*
* These routes are handled by the Worker BEFORE static assets due to
* "run_worker_first": ["/api/*"] in wrangler.jsonc
*/
app.get('/api/hello', (c) => {
return c.json({
message: 'Hello from Cloudflare Workers!',
timestamp: new Date().toISOString(),
})
})
app.get('/api/data', (c) => {
return c.json({
items: [
{ id: 1, name: 'Item 1', description: 'First item' },
{ id: 2, name: 'Item 2', description: 'Second item' },
{ id: 3, name: 'Item 3', description: 'Third item' },
],
count: 3,
})
})
app.post('/api/echo', async (c) => {
const body = await c.req.json()
return c.json({
received: body,
method: c.req.method,
})
})
/**
* Health check endpoint
*/
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 will be served from the public/ directory
* thanks to Workers Static Assets
*/
app.all('*', (c) => {
// Let Cloudflare Workers handle static assets automatically
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
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */
/* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
"target": "es2021",
/* Specify a set of bundled library declaration files that describe the target runtime environment. */
"lib": ["es2021"],
/* Specify what JSX code is generated. */
"jsx": "react-jsx",
/* Specify what module code is generated. */
"module": "es2022",
/* Specify how TypeScript looks up a file from a given module specifier. */
"moduleResolution": "Bundler",
/* Enable importing .json files */
"resolveJsonModule": true,
/* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
"allowJs": true,
/* Enable error reporting in type-checked JavaScript files. */
"checkJs": false,
/* Disable emitting files from a compilation. */
"noEmit": true,
/* Ensure that each file can be safely transpiled without relying on other imports. */
"isolatedModules": true,
/* Allow 'import x from y' when a module doesn't have a default export. */
"allowSyntheticDefaultImports": true,
/* Ensure that casing is correct in imports. */
"forceConsistentCasingInFileNames": true,
/* Enable all strict type-checking options. */
"strict": true,
/* Skip type checking all .d.ts files. */
"skipLibCheck": true,
"types": [
"./worker-configuration.d.ts"
]
},
"exclude": ["test"],
"include": ["worker-configuration.d.ts", "src/**/*.ts"]
}
import { defineConfig } from 'vite'
import { cloudflare } from '@cloudflare/vite-plugin'
export default defineConfig({
plugins: [
cloudflare({
// Optional: Configure the plugin if needed
// See: https://developers.cloudflare.com/workers/vite-plugin/
}),
],
})
/**
* For more details on how to configure Wrangler, refer to:
* https://developers.cloudflare.com/workers/wrangler/configuration/
*/
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "cloudflare-worker-base-test",
"main": "src/index.ts",
"account_id": "0460574641fdbb98159c98ebf593e2bd",
"compatibility_date": "2025-10-11",
"observability": {
"enabled": true
},
/**
* Static Assets
* https://developers.cloudflare.com/workers/static-assets/
*
* CRITICAL: run_worker_first prevents SPA fallback from intercepting API routes
* See: https://github.com/cloudflare/workers-sdk/issues/8879
*/
"assets": {
"directory": "./public/",
"binding": "ASSETS",
"not_found_handling": "single-page-application",
"run_worker_first": ["/api/*"]
}
/**
* Smart Placement
* Docs: https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement
*/
// "placement": { "mode": "smart" }
/**
* Bindings
* Bindings allow your Worker to interact with resources on the Cloudflare Developer Platform, including
* databases, object storage, AI inference, real-time communication and more.
* https://developers.cloudflare.com/workers/runtime-apis/bindings/
*/
/**
* Environment Variables
* https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables
*/
// "vars": { "MY_VARIABLE": "production_value" }
/**
* Note: Use secrets to store sensitive data.
* https://developers.cloudflare.com/workers/configuration/secrets/
*/
/**
* Service Bindings (communicate between multiple Workers)
* https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings
*/
// "services": [{ "binding": "MY_SERVICE", "service": "my-service" }]
}