Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:24:36 +08:00
commit ca8c7fd975
15 changed files with 2860 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
{
"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"
},
"skills": [
"./"
]
}

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
# cloudflare-worker-base
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.

195
SKILL.md Normal file
View File

@@ -0,0 +1,195 @@
---
name: cloudflare-worker-base
description: |
Set up Cloudflare Workers with Hono routing, Vite plugin, and Static Assets using production-tested patterns.
Prevents 8 errors: export syntax, routing conflicts, HMR crashes, gradual rollout asset mismatches, and free tier 429s.
Use when: creating Workers projects, configuring Hono or Vite for Workers, deploying with Wrangler,
adding Static Assets with SPA fallback, or troubleshooting export syntax, API route conflicts, scheduled
handlers, or HMR race conditions.
Keywords: Cloudflare Workers, CF Workers, Hono, wrangler, Vite, Static Assets, @cloudflare/vite-plugin,
wrangler.jsonc, ES Module, run_worker_first, SPA fallback, API routes, serverless, edge computing,
"Cannot read properties of undefined", "Static Assets 404", "A hanging Promise was canceled",
"Handler does not export", deployment fails, routing not working, HMR crashes
license: MIT
---
# Cloudflare Worker Base Stack
**Production-tested**: cloudflare-worker-base-test (https://cloudflare-worker-base-test.webfonts.workers.dev)
**Last Updated**: 2025-11-24
**Status**: Production Ready ✅
**Latest Versions**: hono@4.10.6, @cloudflare/vite-plugin@1.15.2, vite@7.2.4, wrangler@4.50.0
**Recent Updates (2025)**:
- **March 2025**: Wrangler v4 release (minimal breaking changes, v3 supported until Q1 2027)
- **June 2025**: Native Integrations removed from dashboard (CLI-based approach with wrangler secrets)
- **2025 Platform**: Workers VPC Services, Durable Objects Data Studio, 64 env vars (5KB each), unlimited Cron Triggers per Worker, WebSocket 32 MiB messages, node:fs/Web File System APIs
- **2025 Static Assets**: Gradual rollout asset mismatch issue, free tier 429 errors with run_worker_first, Vite plugin auto-detection
- **Hono 4.10.x**: Enhanced TypeScript RPC type inference, cloneRawRequest utility, JWT aud validation, auth middleware improvements
---
## Quick Start (5 Minutes)
```bash
# 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.6
npm install -D @cloudflare/vite-plugin@1.15.2 vite@7.2.4
# 3. Create wrangler.jsonc
{
"name": "my-worker",
"main": "src/index.ts",
"account_id": "YOUR_ACCOUNT_ID",
"compatibility_date": "2025-11-11",
"assets": {
"directory": "./public/",
"binding": "ASSETS",
"not_found_handling": "single-page-application",
"run_worker_first": ["/api/*"] // CRITICAL: Prevents SPA fallback from intercepting API routes
}
}
# 4. Create vite.config.ts
import { defineConfig } from 'vite'
import { cloudflare } from '@cloudflare/vite-plugin'
export default defineConfig({ plugins: [cloudflare()] })
# 5. Create src/index.ts
import { Hono } from 'hono'
type Bindings = { ASSETS: Fetcher }
const app = new Hono<{ Bindings: Bindings }>()
app.get('/api/hello', (c) => c.json({ message: 'Hello!' }))
app.all('*', (c) => c.env.ASSETS.fetch(c.req.raw))
export default app // CRITICAL: Use this pattern (NOT { fetch: app.fetch })
# 6. Deploy
npm run dev # Local: http://localhost:8787
wrangler deploy # Production
```
**Critical Configuration**:
- `run_worker_first: ["/api/*"]` - Without this, SPA fallback intercepts API routes returning `index.html` instead of JSON ([workers-sdk #8879](https://github.com/cloudflare/workers-sdk/issues/8879))
- `export default app` - Using `{ fetch: app.fetch }` causes "Cannot read properties of undefined" ([honojs/hono #3955](https://github.com/honojs/hono/issues/3955))
## Known Issues Prevention
This skill prevents **8 documented issues**:
### Issue #1: Export Syntax Error
**Error**: "Cannot read properties of undefined (reading 'map')"
**Source**: [honojs/hono #3955](https://github.com/honojs/hono/issues/3955)
**Prevention**: Use `export default app` (NOT `{ fetch: app.fetch }`)
### Issue #2: Static Assets Routing Conflicts
**Error**: API routes return `index.html` instead of JSON
**Source**: [workers-sdk #8879](https://github.com/cloudflare/workers-sdk/issues/8879)
**Prevention**: Add `"run_worker_first": ["/api/*"]` to wrangler.jsonc
### Issue #3: Scheduled/Cron Not Exported
**Error**: "Handler does not export a scheduled() function"
**Source**: [honojs/vite-plugins #275](https://github.com/honojs/vite-plugins/issues/275)
**Prevention**: Use Module Worker format when needed:
```typescript
export default {
fetch: app.fetch,
scheduled: async (event, env, ctx) => { /* ... */ }
}
```
### Issue #4: HMR Race Condition
**Error**: "A hanging Promise was canceled" during development
**Source**: [workers-sdk #9518](https://github.com/cloudflare/workers-sdk/issues/9518)
**Prevention**: Use `@cloudflare/vite-plugin@1.13.13` or later
### Issue #5: Static Assets Upload Race
**Error**: Non-deterministic deployment failures in CI/CD
**Source**: [workers-sdk #7555](https://github.com/cloudflare/workers-sdk/issues/7555)
**Prevention**: Use Wrangler 4.x+ with retry logic (fixed in recent versions)
### Issue #6: Service Worker Format Confusion
**Error**: Using deprecated Service Worker format
**Source**: Cloudflare migration guide
**Prevention**: Always use ES Module format
### Issue #7: Gradual Rollouts Asset Mismatch (2025)
**Error**: 404 errors for fingerprinted assets during gradual deployments
**Source**: [Cloudflare Static Assets Docs](https://developers.cloudflare.com/workers/static-assets/routing/advanced/gradual-rollouts)
**Why It Happens**: Modern frameworks (React/Vue/Angular with Vite) generate fingerprinted filenames (e.g., `index-a1b2c3d4.js`). During gradual rollouts between versions, a user's initial request may go to Version A (HTML references `index-a1b2c3d4.js`), but subsequent asset requests route to Version B (only has `index-m3n4o5p6.js`), causing 404s
**Prevention**:
- Avoid gradual deployments with fingerprinted assets
- Use instant cutover deployments for static sites
- Or implement version-aware routing with custom logic
### Issue #8: Free Tier 429 Errors with run_worker_first (2025)
**Error**: 429 (Too Many Requests) responses on asset requests when exceeding free tier limits
**Source**: [Cloudflare Static Assets Billing Docs](https://developers.cloudflare.com/workers/static-assets/billing-and-limitations)
**Why It Happens**: When using `run_worker_first`, requests matching specified patterns ALWAYS invoke your Worker script (counted toward free tier limits). After exceeding limits, these requests receive 429 instead of falling back to free static asset serving
**Prevention**:
- Upgrade to Workers Paid plan ($5/month) for unlimited requests
- Use negative patterns (`!/pattern`) to exclude paths from Worker invocation
- Minimize `run_worker_first` patterns to only essential API routes
## Route Priority with run_worker_first
**Critical Understanding**: `"not_found_handling": "single-page-application"` returns `index.html` for unknown routes (enables React Router, Vue Router). Without `run_worker_first`, this intercepts API routes!
**Request Routing with `run_worker_first: ["/api/*"]`**:
1. `/api/hello` → Worker handles (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)
**Static Assets Caching**: Automatic edge caching. Cache bust with query strings: `<link href="/styles.css?v=1.0.0">`
**Free Tier Warning** (2025): `run_worker_first` patterns count toward free tier limits. After exceeding, requests get 429 instead of falling back to free static assets. Use negative patterns (`!/pattern`) or upgrade to Paid plan.
## Bundled Resources
**Templates**: Complete setup files in `templates/` directory (wrangler.jsonc, vite.config.ts, package.json, tsconfig.json, src/index.ts, public/index.html, styles.css, script.js)
## 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**: https://developers.cloudflare.com/workers/wrangler/
- **Hono**: https://hono.dev/docs/getting-started/cloudflare-workers
- **MCP Tool**: Use `mcp__cloudflare-docs__search_cloudflare_documentation` for latest docs
---
## Dependencies (Latest Verified 2025-11-24)
```json
{
"dependencies": {
"hono": "^4.10.6"
},
"devDependencies": {
"@cloudflare/vite-plugin": "^1.15.2",
"@cloudflare/workers-types": "^4.20251121.0",
"vite": "^7.2.4",
"wrangler": "^4.50.0",
"typescript": "^5.9.3"
}
}
```
---
## Production Validation
**Live Example**: https://cloudflare-worker-base-test.webfonts.workers.dev (build time: 45 min, 0 errors, all 8 issues prevented)

89
plugin.lock.json Normal file
View File

@@ -0,0 +1,89 @@
{
"$schema": "internal://schemas/plugin.lock.v1.json",
"pluginId": "gh:jezweb/claude-skills:skills/cloudflare-worker-base",
"normalized": {
"repo": null,
"ref": "refs/tags/v20251128.0",
"commit": "b61bb695bc7ea03aa2d6a1257968e8c79c19f582",
"treeHash": "d10a39858a7c02c469ba337d1de58a851f9b46fafbdfbe9b5493c53f9894fe32",
"generatedAt": "2025-11-28T10:18:55.745950Z",
"toolVersion": "publish_plugins.py@0.2.0"
},
"origin": {
"remote": "git@github.com:zhongweili/42plugin-data.git",
"branch": "master",
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
},
"manifest": {
"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"
},
"content": {
"files": [
{
"path": "README.md",
"sha256": "4056f75b72af927d3b0f5bb4e1a8871e71531756210491f1d8a6c83e951020cb"
},
{
"path": "SKILL.md",
"sha256": "1a326141fce783e31db73111cb99173f4281cac2be8c65c9c0536148b676f82a"
},
{
"path": "references/architecture.md",
"sha256": "d3fd27f33955a41dd4ef73fea6dac6d36b37807c4a291d2e06c8f2f43d9d4e3d"
},
{
"path": "references/common-issues.md",
"sha256": "e9cec8e56157555b9fb0505468d521cf06245745db80f325ab796ad0ace92e3e"
},
{
"path": "references/deployment.md",
"sha256": "d9b4f83effbc5ea3f1f7f2a56e4ac53057245a3c8b11ce6b7bcbfc2a73c5ce19"
},
{
"path": ".claude-plugin/plugin.json",
"sha256": "2c75f5bcf8d188a482bf67dac1e7def7e34b3825e47c762beae7d21cbfd6f1d7"
},
{
"path": "templates/wrangler.jsonc",
"sha256": "dba230edb1754e4bfe4f1adf5039f1a9f52c4c912c09d3101856b7f8b4c89bd7"
},
{
"path": "templates/package.json",
"sha256": "5778d01947d2f62e046415c65ccfa8f29388bba9fb5f92e9241336a86de7e308"
},
{
"path": "templates/tsconfig.json",
"sha256": "6956104e94fa7add588ceb73718ee8dac3e44c1427b6e9c647e1a9961b1873e6"
},
{
"path": "templates/vite.config.ts",
"sha256": "5d2062acc1fa802f8ca1fe41ca07ef45d4ec97fe6efef83dc8e03ff8fad37fe2"
},
{
"path": "templates/public/index.html",
"sha256": "666f828cfb91306baf0b318cbcc33cfdb703013a0c4ea54af0ff50ad641f9fa7"
},
{
"path": "templates/public/styles.css",
"sha256": "8bff3ff271f32820ac2e3d0493caa3aa59c99165ce11af52cf3fa59e49d772d3"
},
{
"path": "templates/public/script.js",
"sha256": "cc4fa2c001fb8b12fbb95e91d9bdbd498d0aab56305fa7b3b0d6023bd19268f8"
},
{
"path": "templates/src/index.ts",
"sha256": "c0a8b998f0e1d90261d4ca9d7e81513ce5fd92922aca61cf71020eac56162b10"
}
],
"dirSha256": "d10a39858a7c02c469ba337d1de58a851f9b46fafbdfbe9b5493c53f9894fe32"
},
"security": {
"scannedAt": null,
"scannerVersion": null,
"flags": []
}
}

529
references/architecture.md Normal file
View File

@@ -0,0 +1,529 @@
# 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](#export-patterns)
2. [Routing Architecture](#routing-architecture)
3. [Static Assets Integration](#static-assets-integration)
4. [Bindings and Type Safety](#bindings-and-type-safety)
5. [Development vs Production](#development-vs-production)
---
## Export Patterns
### The Correct Pattern (ES Module Format)
```typescript
import { Hono } from 'hono'
const app = new Hono()
// Define routes...
// ✅ CORRECT: Export the Hono app directly
export default app
```
**Why this works:**
- Hono's app object already implements the `fetch` handler
- When Cloudflare calls your Worker, it automatically invokes `app.fetch()`
- This is the **ES Module Worker format** (modern, recommended)
### The Incorrect Pattern (Causes Errors)
```typescript
// ❌ 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.fetch` binding is lost
- The Vite bundler transforms the code in a way that breaks the `this` context
- Source: [honojs/hono #3955](https://github.com/honojs/hono/issues/3955)
### Module Worker Format (When You Need Multiple Handlers)
```typescript
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)
```typescript
// ❌ 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.html
```
### Configuration Required
In `wrangler.jsonc`:
```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`)
```typescript
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-is
```
3. **SPA fallback** (if file doesn't exist)
```
/unknown-route → Returns public/index.html
```
### Advanced Routing Patterns
#### Wildcard Routes
```typescript
// 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
```typescript
// 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
```typescript
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
```typescript
// 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
```typescript
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
```typescript
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
```typescript
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
```typescript
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`:
```typescript
// Auto-generated by Wrangler
interface Env {
ASSETS: Fetcher
MY_KV: KVNamespace
DB: D1Database
MY_BUCKET: R2Bucket
MY_VAR: string
}
```
Then use:
```typescript
const app = new Hono<{ Bindings: Env }>()
```
---
## Development vs Production
### Local Development (wrangler dev)
```bash
npm run dev
```
**What happens:**
- Miniflare simulates Cloudflare's runtime locally
- Bindings are emulated (KV, D1, R2)
- HMR enabled via Vite plugin
- Runs on http://localhost:8787
**Configuration**:
```typescript
// vite.config.ts
export default defineConfig({
plugins: [
cloudflare({
persist: true, // Persist data between restarts
}),
],
})
```
### Production Deployment (wrangler deploy)
```bash
npm run deploy
```
**What 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 artifacts
```
### Environment-Specific Configuration
```jsonc
// 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:
```bash
wrangler deploy --env staging
wrangler deploy --env production
```
### Environment Detection in Code
```typescript
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
```typescript
import { logger } from 'hono/logger'
import { cors } from 'hono/cors'
app.use('*', logger())
app.use('/api/*', cors())
```
### 2. Separate API and Static Routes
```typescript
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
```typescript
app.onError((err, c) => {
console.error(err)
return c.json({ error: 'Internal Server Error' }, 500)
})
```
### 4. Use TypeScript
```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
```typescript
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`:
```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:
```bash
npm install -D @cloudflare/vite-plugin@1.13.13
```
### Issue: 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`.

645
references/common-issues.md Normal file
View File

@@ -0,0 +1,645 @@
# 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](#issue-1-export-syntax-error)
2. [Issue #2: Static Assets Routing Conflicts](#issue-2-static-assets-routing-conflicts)
3. [Issue #3: Scheduled/Cron Not Exported](#issue-3-scheduledcron-not-exported)
4. [Issue #4: HMR Race Condition](#issue-4-hmr-race-condition)
5. [Issue #5: Static Assets Upload Race](#issue-5-static-assets-upload-race)
6. [Issue #6: Service Worker Format Confusion](#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](https://github.com/honojs/hono/issues/3955)
- **Related**: [honojs/vite-plugins #237](https://github.com/honojs/vite-plugins/issues/237)
- **Reported**: February 2025
### Root Cause
When using Hono with Vite's build tools, the incorrect export pattern breaks the `this` context:
```typescript
// ❌ WRONG: This causes the error
export default {
fetch: app.fetch
}
```
**Why it breaks:**
- Vite's bundler transforms the code
- The `app.fetch` binding loses its `this` context
- When Cloudflare calls `fetch()`, `this` is `undefined`
- Hono tries to access `this.routes.map(...)` → Error
### Fix
Use the direct export pattern:
```typescript
import { Hono } from 'hono'
const app = new Hono()
// Define routes...
// ✅ CORRECT
export default app
```
**Why 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:
```typescript
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.html` instead 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
```bash
curl http://localhost:8787/api/hello
# Expected: {"message":"Hello"}
# Actual: <!DOCTYPE html><html>...
```
### Source
- **GitHub Issue**: [workers-sdk #8879](https://github.com/cloudflare/workers-sdk/issues/8879)
- **Reported**: April 2025
### Root Cause
The `not_found_handling: "single-page-application"` configuration creates a fallback:
```
Request → File not found → Return index.html
```
**Without `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`:
```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
```jsonc
{
"assets": {
"run_worker_first": [
"/api/*",
"/auth/*",
"/webhooks/*",
"/_app/*"
]
}
}
```
### How to Verify Fix
1. Start dev server: `npm run dev`
2. Test API endpoint:
```bash
curl -i http://localhost:8787/api/hello
```
3. Check response:
- ✅ `Content-Type: application/json`
- ✅ JSON body
4. Test static file:
```bash
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() function
```
Deployment succeeds, but cron triggers fail.
### Source
- **GitHub Issue**: [honojs/vite-plugins #275](https://github.com/honojs/vite-plugins/issues/275)
- **Reported**: July 2025
### Root Cause
The `@hono/vite-build/cloudflare-workers` plugin **only supports the `fetch` handler**.
If you use:
```typescript
export default app // Only exports fetch handler
```
...then scheduled/tail handlers are not exported.
### Fix Option 1: Use Module Worker Format
```typescript
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:
```bash
npm uninstall @hono/vite-build
npm install -D @cloudflare/vite-plugin
```
Update `vite.config.ts`:
```typescript
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
```jsonc
{
"triggers": {
"crons": ["0 0 * * *"] // Daily at midnight UTC
}
}
```
### How to Verify Fix
1. Deploy: `npm run deploy`
2. Trigger manually:
```bash
wrangler deploy && wrangler tail
```
3. 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](https://github.com/cloudflare/workers-sdk/issues/9518)
- **Related**: [workers-sdk #9249](https://github.com/cloudflare/workers-sdk/issues/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`:
```bash
npm install -D @cloudflare/vite-plugin@1.13.13
```
**Fixed in version 1.13.13** (October 2025)
### Alternative: Configure Vite with Persistence
If updating doesn't fix it, try:
```typescript
// 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:
```bash
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](https://github.com/cloudflare/workers-sdk/issues/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:
```bash
npm install -D wrangler@latest
```
**Improvements 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
```yaml
# GitHub Actions example
- name: Deploy to Cloudflare
run: |
for i in {1..3}; do
npm run deploy && break || sleep 10
done
```
```bash
# Shell script
#!/bin/bash
for i in {1..3}; do
npm run deploy && break || sleep 10
done
```
### Fix Option 3: Reduce Asset Count
If you have many small files, bundle them:
```typescript
// vite.config.ts
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
},
},
},
},
})
```
### How to Verify Fix
1. Deploy locally 5 times:
```bash
for i in {1..5}; do npm run deploy; done
```
2. 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:
```typescript
// ❌ 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
```typescript
// ✅ CORRECT: ES Module format
export default {
fetch(request, env, ctx) {
return new Response('Hello World')
}
}
```
**With Hono:**
```typescript
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => c.text('Hello World'))
export default app
```
### Migration Steps
1. **Remove `addEventListener`**:
```diff
- addEventListener('fetch', (event) => {
- event.respondWith(handleRequest(event.request))
- })
```
2. **Change to ES Module export**:
```diff
+ export default {
+ fetch(request, env, ctx) {
+ return handleRequest(request, env)
+ }
+ }
```
3. **Update function signatures** to accept `env`:
```diff
- 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`**:
```bash
# 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:
```typescript
const value = await env.MY_KV.get('key')
```
3. TypeScript types work:
```typescript
type Bindings = {
MY_KV: KVNamespace
}
```
---
## General Troubleshooting Tips
### Check Package Versions
```bash
npm list hono @cloudflare/vite-plugin wrangler
```
**Expected (as of 2025-10-20):**
- `hono@4.10.1`
- `@cloudflare/vite-plugin@1.13.13`
- `wrangler@4.43.0`
### Clear Wrangler Cache
```bash
rm -rf node_modules/.wrangler
rm -rf .wrangler
npm run dev
```
### Check Wrangler Config
```bash
wrangler whoami # Verify authentication
wrangler dev --local # Test without deploying
```
### Enable Verbose Logging
```bash
WRANGLER_LOG=debug npm run dev
WRANGLER_LOG=debug npm run deploy
```
### Check 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
```bash
# 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**

856
references/deployment.md Normal file
View File

@@ -0,0 +1,856 @@
# 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](#prerequisites)
2. [Wrangler Commands](#wrangler-commands)
3. [Environment Configuration](#environment-configuration)
4. [CI/CD Pipelines](#cicd-pipelines)
5. [Production Best Practices](#production-best-practices)
6. [Monitoring and Logs](#monitoring-and-logs)
---
## Prerequisites
### 1. Cloudflare Account
Sign up at https://dash.cloudflare.com/sign-up
### 2. Get Account ID
```bash
# Option 1: From dashboard
# Go to: Workers & Pages → Overview → Account ID (right sidebar)
# Option 2: Via Wrangler
wrangler whoami
```
Add to `wrangler.jsonc`:
```jsonc
{
"account_id": "YOUR_ACCOUNT_ID_HERE"
}
```
### 3. Authenticate Wrangler
```bash
# 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
```bash
# 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 staging
```
### Deployment
```bash
# 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-11
```
### Type Generation
```bash
# Generate TypeScript types for bindings
wrangler types
# Output to custom file
wrangler types --output-file=types/worker.d.ts
```
### Logs
```bash
# 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 json
```
### Deployments
```bash
# List recent deployments
wrangler deployments list
# View specific deployment
wrangler deployments view DEPLOYMENT_ID
# Rollback to previous deployment
wrangler rollback --deployment-id DEPLOYMENT_ID
```
### Secrets
```bash
# 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_SECRET
```
### KV Operations
```bash
# 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_ID
```
### D1 Operations
```bash
# 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 list
```
### R2 Operations
```bash
# 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)
```jsonc
{
"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
```jsonc
{
"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:
```bash
wrangler deploy --env staging
wrangler deploy --env production
```
### Environment Detection in Code
```typescript
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`:
```yaml
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**:
```yaml
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 production
```
**Add 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`:
```yaml
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: production
```
**Add 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`:
```bash
#!/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:
```bash
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`:
```jsonc
{
"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
```jsonc
{
"observability": {
"enabled": true
}
}
```
**Provides**:
- Real-time metrics
- Error tracking
- Performance monitoring
### 3. Set Resource Limits
```jsonc
{
"limits": {
"cpu_ms": 50 // Maximum CPU time per request (paid plan)
}
}
```
### 4. Configure Custom Domains
```jsonc
{
"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
```bash
# Never commit secrets to git
wrangler secret put API_KEY
wrangler secret put DATABASE_URL
```
```typescript
// Access in code
const apiKey = c.env.API_KEY
```
### 6. Implement Rate Limiting
```typescript
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
```typescript
app.get('/health', (c) => {
return c.json({
status: 'ok',
version: '1.0.0',
timestamp: new Date().toISOString()
})
})
```
### 8. Implement Error Tracking
```typescript
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
```typescript
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
```bash
# 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
```bash
# 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 json
```
### Analytics Dashboard
View in Cloudflare Dashboard:
1. Workers & Pages → Your Worker → Metrics
2. See:
- Requests per second
- Errors
- CPU time
- Response time
### Custom Metrics
```typescript
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**:
```typescript
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**:
```typescript
// 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
```bash
# List recent deployments
wrangler deployments list
# Rollback to specific deployment
wrangler rollback --deployment-id DEPLOYMENT_ID
```
### Gradual Rollout
```jsonc
{
"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
```bash
# 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
```typescript
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
```typescript
// ❌ 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
```bash
# Check configuration
wrangler deploy --dry-run
# Verbose output
WRANGLER_LOG=debug wrangler deploy
# Check account access
wrangler whoami
```
### Build Errors
```bash
# Clear cache
rm -rf node_modules/.wrangler
rm -rf .wrangler
# Reinstall dependencies
npm ci
# Try again
npm run deploy
```
### Routes Not Working
```bash
# 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**

23
templates/package.json Normal file
View File

@@ -0,0 +1,23 @@
{
"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"
}
}

View File

@@ -0,0 +1,55 @@
<!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>

View File

@@ -0,0 +1,83 @@
/**
* 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',
})
})

178
templates/public/styles.css Normal file
View File

@@ -0,0 +1,178 @@
* {
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%;
}
}

84
templates/src/index.ts Normal file
View File

@@ -0,0 +1,84 @@
/**
* 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

45
templates/tsconfig.json Normal file
View File

@@ -0,0 +1,45 @@
{
"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"]
}

11
templates/vite.config.ts Normal file
View File

@@ -0,0 +1,11 @@
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/
}),
],
})

52
templates/wrangler.jsonc Normal file
View File

@@ -0,0 +1,52 @@
/**
* 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" }]
}