TUTORIAL · 4 MIN STACK

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.

MO
Maya Okafor
Indie hacker · Tutorials & ship-fast guides
TL;DR

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:

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:8787Hello 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

Bottom line

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