Building a cloudflare workers hono chatbot is the fastest way to put a serverless LLM front end on the edge. This tutorial scaffolds a Hono app, wires it to an OpenAI-compatible inference gateway, and ships it to Cloudflare with Wrangler.
Prerequisites
- Node.js 20+ (for local dev)
- Wrangler 3.78+ (
npm i -g wrangler) - TypeScript familiarity
- An API key from n4n.ai (or any OpenAI-compatible gateway)
- A Cloudflare account
If you haven’t used Wrangler before, run wrangler login first.
Scaffold the project
mkdir hono-chatbot && cd hono-chatbot
npm init -y
npm install hono
npm install -D wrangler typescript @cloudflare/workers-types
Create a tsconfig.json tuned for Workers:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"types": ["@cloudflare/workers-types"],
"lib": ["ES2022"],
"strict": true
},
"include": ["src"]
}
Configure Wrangler
Create wrangler.toml. Keep secrets out of version control.
name = "hono-chatbot"
main = "src/index.ts"
compatibility_date = "2024-09-23"
Set the API key as a secret:
wrangler secret put N4N_API_KEY
# paste your key when prompted
Build the Hono API
Create src/index.ts. The cloudflare workers hono chatbot needs a POST endpoint that forwards messages. We target n4n.ai’s OpenAI-compatible endpoint, which addresses 240+ models and provides automatic fallback when a provider is degraded.
import { Hono } from 'hono'
type Bindings = {
N4N_API_KEY: string
}
const app = new Hono<{ Bindings: Bindings }>()
app.post('/api/chat', async (c) => {
const body = await c.req.json<{ messages: { role: string; content: string }[] }>()
if (!body.messages || !Array.isArray(body.messages)) {
return c.json({ error: 'messages array required' }, 400)
}
const upstream = await fetch('https://api.n4n.ai/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${c.env.N4N_API_KEY}`,
},
body: JSON.stringify({
model: 'anthropic/claude-3.5-sonnet',
messages: body.messages,
temperature: 0.7,
}),
})
if (!upstream.ok) {
const text = await upstream.text()
return c.json({ error: 'upstream failed', detail: text }, 502)
}
const data = await upstream.json()
return c.json(data)
})
The Bindings type gives you typed access to c.env.N4N_API_KEY. Choose any model ID the gateway supports; the request shape is plain OpenAI.
Serve a minimal chat UI
A chatbot needs a surface. Add a GET route that returns a single-file HTML page with a textarea and fetch call.
app.get('/', (c) => {
return c.html(`<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Worker Chatbot</title>
<style>body{font:16px sans-serif;max-width:640px;margin:2rem auto}#log{white-space:pre-wrap;border:1px solid #ccc;padding:1rem;min-height:200px}button{margin-top:.5rem}</style>
</head>
<body>
<h1>Cloudflare Workers Hono Chatbot</h1>
<div id="log"></div>
<input id="msg" placeholder="Type a message" style="width:80%" />
<button onclick="send()">Send</button>
<script>
let history = [];
async function send() {
const input = document.getElementById('msg');
const text = input.value.trim();
if (!text) return;
history.push({role:'user', content:text});
document.getElementById('log').textContent += '\\nYou: ' + text + '\\n';
input.value = '';
const res = await fetch('/api/chat', {
method:'POST',
headers:{'Content-Type':'application/json'},
body: JSON.stringify({messages: history})
});
const data = await res.json();
const reply = data.choices?.[0]?.message?.content ?? 'No response';
history.push({role:'assistant', content:reply});
document.getElementById('log').textContent += 'Bot: ' + reply + '\\n';
}
</script>
</body></html>`)
})
export default app
Run locally
Start the dev server:
wrangler dev
In another shell, test the API directly:
curl -X POST http://localhost:8787/api/chat \
-H 'Content-Type: application/json' \
-d '{"messages":[{"role":"user","content":"Say hello in one word"}]}'
Expected output (truncated):
{
"id": "chatcmpl-...",
"object": "chat.completion",
"choices": [
{
"message": {
"role": "assistant",
"content": "Hello"
},
"finish_reason": "stop"
}
]
}
Open http://localhost:8787/ in a browser, type a message, and see the reply rendered. The cloudflare workers hono chatbot now works end-to-end on your machine.
Deploy
Ship it:
wrangler deploy
Wrangler prints the *.workers.dev subdomain. Visit it; the cloudflare workers hono chatbot is live on the edge.
Operational notes
The pattern above blocks on the upstream HTTP call. For production, add streaming to avoid timeout on long generations:
app.post('/api/chat/stream', async (c) => {
const body = await c.req.json<{ messages: any[] }>()
const upstream = await fetch('https://api.n4n.ai/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${c.env.N4N_API_KEY}`,
},
body: JSON.stringify({ model: 'anthropic/claude-3.5-sonnet', messages: body.messages, stream: true }),
})
return new Response(upstream.body, {
headers: { 'Content-Type': 'text/event-stream' },
})
})
Wire the frontend to read the SSE stream. That cuts time-to-first-token from seconds to sub-second.
Cache static HTML at the edge with the Cache API if you serve many users. The chatbot route itself should never be cached.
Honor client routing directives: if you pass model from the browser, validate it server-side to avoid abuse. The gateway forwards provider cache-control hints, so repeated identical prompts may hit provider prompt caches.
Why Hono
Hono’s tiny footprint (≈15 KB) and built-in helpers (c.json, c.html) fit the Cloudflare Workers CPU limit. Express would blow the bundle. The cloudflare workers hono chatbot stack compiles to a single ES module with no native deps.
Troubleshooting
500on deploy: checkwrangler secret listto confirmN4N_API_KEYexists in the target environment.- CORS: if you later split frontend/backend, add
app.use('/api/*', cors())fromhono/cors. - Model not found: the gateway returns 400 with a list of available model IDs; adjust the
modelfield.
That’s the full path from npm init to a running edge chatbot.