# Architecture Deep Dive

> This document explains the architectural patterns used in Cloudflare Workers with Hono, Vite, and Static Assets.

- Skill: `tools-only/architecture-deep-dive` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add tools-only/architecture-deep-dive`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tools-only/architecture-deep-dive/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: tools-only (https://skillmd.com/u/tools-only)
- Updated: 2026-09-29
- Page: https://skillmd.com/skills/tools-only/architecture-deep-dive

---

# 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`.

