From npm create to wrangler deploy in 4 minutes: a Hono + D1 + R2 walkthrough.
If you've never shipped on Cloudflare before, this is the fastest path I know. Four minutes, three commands, one production URL.
Cloudflare Workers + Hono + D1 + R2 is the fastest way to get a real backend running on a real production URL. No Docker, no AWS console, no Terraform. I timed myself: 4 min 12 s from scaffold to live URL with a database and file storage. Here's the exact path.
What you'll have at the end
A Hono API on a real workers.dev URL with:
- A
POST /uploadendpoint that stores files in R2 - A
POST /notesendpoint that writes to a D1 database - A
GET /notesthat reads them back
Three endpoints, persistent storage, deployed to the edge in 200+ cities. For free.
Prereqs (30 seconds)
npm i -g wrangler
wrangler login
That OAuth flow is the only "click around in the browser" step. After this it's all CLI.
Step 1 — Scaffold the Hono app (45 s)
npm create hono@latest my-api
# choose: cloudflare-workers
cd my-api
npm install
npm run dev
You now have a Hono Worker running on localhost:8787. Hit it: curl localhost:8787 → Hello Hono!. That's your starting point.
Step 2 — Add a D1 database (60 s)
wrangler d1 create my-notes
It prints a binding block. Paste it into wrangler.toml:
[[d1_databases]]
binding = "DB"
database_name = "my-notes"
database_id = "<the-id-it-printed>"
Create the schema in schema.sql:
CREATE TABLE notes (
id TEXT PRIMARY KEY,
body TEXT NOT NULL,
created_at INTEGER NOT NULL
);
Apply it locally and remotely:
wrangler d1 execute my-notes --local --file=./schema.sql
wrangler d1 execute my-notes --remote --file=./schema.sql
Step 3 — Add an R2 bucket (30 s)
wrangler r2 bucket create my-uploads
And the binding in wrangler.toml:
[[r2_buckets]]
binding = "UPLOADS"
bucket_name = "my-uploads"
Step 4 — Wire it up (90 s)
Replace src/index.ts:
import { Hono } from 'hono'
type Env = {
DB: D1Database
UPLOADS: R2Bucket
}
const app = new Hono<{ Bindings: Env }>()
app.post('/notes', async (c) => {
const { body } = await c.req.json<{ body: string }>()
const id = crypto.randomUUID()
await c.env.DB.prepare('INSERT INTO notes (id, body, created_at) VALUES (?, ?, ?)')
.bind(id, body, Date.now())
.run()
return c.json({ id })
})
app.get('/notes', async (c) => {
const r = await c.env.DB.prepare('SELECT * FROM notes ORDER BY created_at DESC LIMIT 50').all()
return c.json(r.results)
})
app.post('/upload', async (c) => {
const f = await c.req.blob()
const key = crypto.randomUUID()
await c.env.UPLOADS.put(key, f.stream())
return c.json({ key })
})
export default app
Test locally:
npm run dev
curl -X POST localhost:8787/notes -d '{"body":"hi"}' -H 'content-type: application/json'
curl localhost:8787/notes
Step 5 — Deploy (15 s)
wrangler deploy
That prints a workers.dev URL. Hit it. It's live. Globally. With a database and a file store. Total time elapsed if you didn't pause to read: about 4 minutes.
4 minutes. Three CLI commands. Zero AWS console tabs. The future got here quietly.
What this stack costs at scale
Workers free tier: 100k requests/day. D1 free tier: 5 GB storage, 25 M reads/day. R2 free tier: 10 GB storage, no egress fees. For a side project, you'll basically never pay. For a serious app, the bill scales linearly and stays absurdly cheap. (See our full pricing breakdown.)
Where to go next
- Add Queues for background jobs
- Add a Durable Object for real-time
- Add a frontend on Pages and link it via
service_bindings - Or — and this is the lazy option — let buildr scaffold the whole thing for you
Three CLI commands and a working backend in production.
The first time I shipped on Cloudflare I spent 20 minutes in the dashboard. Now I never open the dashboard. Wrangler does it all. Try it on a Sunday and tell me how it went.
Skip the wrangler.toml entirely.
buildr generates this same stack from a chat prompt — Hono, D1, R2, your Cloudflare account, deployed. Same code as above, just typed for you.
Build my app free