Skip to content
Back to skills

055 Architecture D3fd27f3

ASecurity

**Last Updated**: 2025-10-20 This document explains the architectural patterns used in Cloudflare Workers with Hono, Vite, and Static Assets. ---

  • 9 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added October 11, 2026
devopstypescriptgobashgitapidatabaseperformance

Works with

  • api

Security analysis

A96/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro shows the line behind each finding and how to fix it

Scanned October 11, 2026

npx -y skills add tools-only/X-Skills --skill 055-architecture_d3fd27f3 --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of 055 Architecture D3fd27f3?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for 055 Architecture D3fd27f3
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/tools-only-055-architecture-d3fd27f3/badge)](https://www.skillsdirectory.com/skills/tools-only-055-architecture-d3fd27f3)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

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

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.