Initial commit
This commit is contained in:
529
references/architecture.md
Normal file
529
references/architecture.md
Normal 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`.
|
||||
Reference in New Issue
Block a user