
Cloudflare Worker Builder
- 1.3k installs
- 946 repo stars
- Updated July 2, 2026
- jezweb/claude-skills
cloudflare-worker-builder is an agent skill for scaffold and deploy cloudflare workers with hono routing, vite plugin, and static assets. describe project, scaffold structure, configure bindings, deploy. use whenever the
About
The cloudflare-worker-builder skill is designed for scaffold and deploy Cloudflare Workers with Hono routing, Vite plugin, and Static Assets. Describe project, scaffold structure, configure bindings, deploy. Use whenever the. Cloudflare Worker Builder Scaffold a working Cloudflare Worker project from a brief description. Produces a deployable project with Hono routing, Vite dev server, and Static Assets. Invoke when the user asks about cloudflare worker builder or related SKILL.md workflows.
- What does the app do? (API only, SPA + API, landing page).
- What data storage? (D1 database, R2 files, KV cache, none).
- Auth needed? (Clerk, better-auth, none).
- Custom domain or workers.dev subdomain?.
- wrangler.jsonc — Worker configuration.
Cloudflare Worker Builder by the numbers
- 1,256 all-time installs (skills.sh)
- +25 installs in the week ending Jul 29, 2026 (Skillselion tracking)
- Ranked #315 of 1,888 Design & UI/UX skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Jul 31, 2026 (Skillselion catalog sync)
cloudflare-worker-builder capabilities & compatibility
- Capabilities
- what does the app do? (api only, spa + api, land · what data storage? (d1 database, r2 files, kv ca · auth needed? (clerk, better auth, none) · custom domain or workers.dev subdomain?
- Use cases
- frontend
What cloudflare-worker-builder says it does
Scaffold and deploy Cloudflare Workers with Hono routing, Vite plugin, and Static Assets. Describe project, scaffold structure, configure bindings, deploy. Use whenever the user wa
Scaffold and deploy Cloudflare Workers with Hono routing, Vite plugin, and Static Assets. Describe project, scaffold structure, configure bindings, deploy. Use
npx skills add https://github.com/jezweb/claude-skills --skill cloudflare-worker-builderAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.3k |
|---|---|
| repo stars | ★ 946 |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 2, 2026 |
| Repository | jezweb/claude-skills ↗ |
How do I scaffold and deploy cloudflare workers with hono routing, vite plugin, and static assets. describe project, scaffold structure, configure bindings, deploy. use whenever the?
Scaffold and deploy Cloudflare Workers with Hono routing, Vite plugin, and Static Assets. Describe project, scaffold structure, configure bindings, deploy. Use whenever the.
Who is it for?
Developers using cloudflare worker builder workflows documented in SKILL.md.
Skip if: Skip when the task falls outside cloudflare-worker-builder scope or needs a different stack.
When should I use this skill?
User asks about cloudflare worker builder or related SKILL.md workflows.
What you get
Completed cloudflare-worker-builder workflow with documented commands, files, and expected deliverables.
- wrangler.toml
- Hono handler code
- Vitest test suite
Files
Cloudflare Worker Builder
Scaffold a working Cloudflare Worker project from a brief description. Produces a deployable project with Hono routing, Vite dev server, and Static Assets.
Workflow
Step 1: Understand the Project
Ask about the project to choose the right bindings and structure:
- What does the app do? (API only, SPA + API, landing page)
- What data storage? (D1 database, R2 files, KV cache, none)
- Auth needed? (Clerk, better-auth, none)
- Custom domain or workers.dev subdomain?
A brief like "todo app with database" is enough to proceed.
Step 2: Scaffold the Project
npm create cloudflare@latest my-worker -- --type hello-world --ts --git --deploy false --framework none
cd my-worker
npm install hono
npm install -D @cloudflare/vite-plugin viteCopy and customise the asset files from this skill's assets/ directory:
wrangler.jsonc— Worker configurationvite.config.ts— Vite + Cloudflare pluginsrc/index.ts— Hono app with Static Assets fallbackpackage.json— Scripts and dependenciestsconfig.json— TypeScript configpublic/index.html— SPA entry point
Step 3: Configure Bindings
Add bindings to wrangler.jsonc based on project needs. Wrangler 4.45+ auto-provisions resources on first deploy — always specify explicit names:
{
"name": "my-worker",
"main": "src/index.ts",
"compatibility_date": "2025-11-11",
"assets": {
"directory": "./public/",
"binding": "ASSETS",
"not_found_handling": "single-page-application",
"run_worker_first": ["/api/*"]
},
// Add as needed:
"d1_databases": [{ "binding": "DB", "database_name": "my-app-db" }],
"r2_buckets": [{ "binding": "STORAGE", "bucket_name": "my-app-files" }],
"kv_namespaces": [{ "binding": "CACHE", "title": "my-app-cache" }]
}Step 4: Deploy
npm run dev # Local dev at http://localhost:8787
wrangler deploy # Production deploy---
Critical Patterns
Export Syntax
// CORRECT — use this pattern
export default app
// WRONG — causes "Cannot read properties of undefined"
export default { fetch: app.fetch }Source: honojs/hono #3955
Static Assets + API Routes
Without run_worker_first, SPA fallback intercepts API routes and returns index.html instead of JSON:
"assets": {
"not_found_handling": "single-page-application",
"run_worker_first": ["/api/*"] // CRITICAL
}Source: workers-sdk #8879
Vite Config
import { defineConfig } from 'vite'
import { cloudflare } from '@cloudflare/vite-plugin'
export default defineConfig({ plugins: [cloudflare()] })Always set the main field in wrangler.jsonc — the Vite plugin needs it.
Scheduled/Cron Handlers
When adding cron triggers, switch to explicit export:
export default {
fetch: app.fetch,
scheduled: async (event, env, ctx) => { /* ... */ }
}---
Reference Files
Read these for detailed troubleshooting:
references/common-issues.md— 10 documented issues with sources and fixesreferences/architecture.md— Route priority, caching, Workers RPCreferences/deployment.md— CI/CD, auto-provisioning, gradual rollouts
{
"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.17.1",
"@cloudflare/vitest-pool-workers": "^0.11.1",
"typescript": "^5.9.3",
"vite": "^7.3.0",
"vitest": "^4.0.0",
"wrangler": "^4.54.0"
},
"dependencies": {
"hono": "^4.11.3"
}
}
<!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" }]
}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.17.1Fixed 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 2026-01-06):
hono@4.11.3@cloudflare/vite-plugin@1.17.1wrangler@4.54.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.17.1+ |
| #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 ✅
Related skills
How it compares
Pick cloudflare-worker-builder over generic serverless scaffolds when the target runtime is Cloudflare Workers with Hono and worker-pool Vitest testing.
FAQ
What does cloudflare-worker-builder do?
Scaffold and deploy Cloudflare Workers with Hono routing, Vite plugin, and Static Assets. Describe project, scaffold structure, configure bindings, deploy. Use whenever the.
When should I use cloudflare-worker-builder?
User asks about cloudflare worker builder or related SKILL.md workflows.
Is cloudflare-worker-builder safe to install?
Review the Security Audits panel on this page before installing in production.