Make sure you have bun installed!
You will also need:
- a GitHub account
- a Fly.io account (free, no credit card)
A tiny web framework- you define routes like, kind of like Express:
- “When someone does
GET /api/stuff, run this function.”
A database thats just a file (like my.db) on your computer, this way things can persist!
A TypeScript ORM that lets you:
- define your tables in
schema.ts - query them with TypeScript instead of raw SQL strings
A CLI tool that takes your schema and creates/updates the real database so you dont have to manually!
You’ll end up with your own project that has:
- a working Hono server
- a SQLite file on disk
- Drizzle schema + queries
- a couple API endpoints that read/write the DB
In this workshop ill show you how to set all this up, what you actually make is up to you, it'd be super cool if it was Christmas themed though!
This is the layout you're aiming for:
beans-cool-api/
.env
drizzle.config.ts
my.db (created after push)
src/
index.ts (Hono server + routes)
db/
schema.ts (table definitions)
index.ts (runtime DB connection)
queries.ts (helper functions: list/create/update/delete)
bun create hono@latest my-project
cd my-project
bun install
bun run devYour terminal prints a URL. Open it.
In src/index.ts:
import { Hono } from "hono"
const app = new Hono()
app.get("/", (c) => c.text("Beans!"))
export default appIf you see “Beans!” when your open the url Hono printed, your server works! Yipeeeee
bun add drizzle-orm dotenv
bun add -D drizzle-kit @types/bunWhat this does:
drizzle-orm= what you use in your codedrizzle-kit= what you run in the terminaldotenv= loads.envfile values
Create .env in the project root:
DB_FILE_NAME=./my.dbThis is the file SQLite will store data in.
Add to .gitignore:
.env
my.db
Think of schema as: the database’s "types":
- table name
- column names
- column types
- default values
Create src/db/schema.ts:
import { sqliteTable, integer, text } from "drizzle-orm/sqlite-core"
export const wishes = sqliteTable("wishes", {
id: integer("id").primaryKey({ autoIncrement: true }),
item: text("item").notNull(),
fulfilled: integer("fulfilled").notNull().default(0),
createdAt: integer("created_at").notNull(),
})Your table could be quests, scores, cookies, notes… anything.
You only need:
- one table to start
- a primary key id is needed per table
- a couple fields you care about
Its up to you what you put in it, below is a cheat sheet!
Primary key (your table needs this, it needs to be unique)
id: integer("id").primaryKey({ autoIncrement: true }),Text / strings
text("name").notNull() // required because of notNull
text("notes") // optional textNumbers / booleans
integer("count").notNull()
integer("done").notNull().default(0) // 0 = false, 1 = true SQLite doesnt have real booleansCreate drizzle.config.ts in project root:
import "dotenv/config"
import { defineConfig } from "drizzle-kit"
export default defineConfig({
out: "./drizzle",
schema: "./src/db/schema.ts",
dialect: "sqlite",
dbCredentials: {
url: process.env.DB_FILE_NAME!,
},
})Run:
bunx drizzle-kit pushAfter this:
- your
my.dbfile should exist - your table should exist inside it
If you change your schema later (add a column), run push again.
This is the part that makes routes actually talk to the database.
Create src/db/index.ts:
import "dotenv/config"
import { drizzle } from "drizzle-orm/bun-sqlite"
export const db = drizzle(process.env.DB_FILE_NAME!)Now the server can use db to query your SQLite file.
Routes should be readable. So we make helper functions like:
listThings()createThing()updateThing()deleteThing()
Create src/db/queries.ts:
import { db } from "./index"
import { wishes } from "./schema"
import { eq, desc } from "drizzle-orm"
export function listWishes() {
return db.select().from(wishes).orderBy(desc(wishes.id)).all()
}
export function createWish(item: string) {
const createdAt = Math.floor(Date.now() / 1000)
const res = db.insert(wishes).values({
item,
fulfilled: 0,
createdAt,
}).run()
return { id: Number(res.lastInsertRowid) }
}
export function fulfillWish(id: number) {
const res = db.update(wishes)
.set({ fulfilled: 1 })
.where(eq(wishes.id, id))
.run()
return { changes: res.changes }
}
export function deleteWish(id: number) {
const res = db.delete(wishes).where(eq(wishes.id, id)).run()
return { changes: res.changes }
}Again: this is an example, do whatever you want!
Now your routes become short and understandable.
In src/index.ts:
import { createWish, deleteWish, fulfillWish, listWishes } from "./db/queries"
app.get("/api/wishes", (c) => c.json(listWishes()))
app.post("/api/wishes", async (c) => {
const body = await c.req.json().catch(() => null)
const item = (body?.item ?? "").toString().trim()
if (!item) return c.json({ error: "item is required" }, 400)
return c.json(createWish(item), 201)
})
app.patch("/api/wishes/:id/fulfill", (c) => {
const id = Number(c.req.param("id"))
if (!Number.isFinite(id)) return c.json({ error: "bad id" }, 400)
const res = fulfillWish(id)
if (res.changes === 0) return c.json({ error: "not found" }, 404)
return c.json({ ok: true })
})
app.delete("/api/wishes/:id", (c) => {
const id = Number(c.req.param("id"))
if (!Number.isFinite(id)) return c.json({ error: "bad id" }, 400)
const res = deleteWish(id)
if (res.changes === 0) return c.json({ error: "not found" }, 404)
return c.json({ ok: true })
})That’s the pattern you can reuse forever:
- validate
- call helper
- return JSON
This is an example of how u can text your APIs!
Add a wish:
curl -X POST http://localhost:8181/api/wishes \
-H "content-type: application/json" \
-d '{"item":"lego"}'List:
curl http://localhost:8181/api/wishesFulfill:
curl -X PATCH http://localhost:8181/api/wishes/1/fulfillDelete:
curl -X DELETE http://localhost:8181/api/wishes/1You now have the entire pipeline:
- define schema
- push schema to DB
- connect DB in runtime
- write queries
- write routes
Hosting providers set the port for you.
You must read process.env.PORT.
At the bottom of src/index.ts, replace the export with:
const port = Number(process.env.PORT) || 3000
export default {
port,
fetch: app.fetch,
}Local dev still works. Deployment will now work too.
bunx drizzle-kit push
bun install -g fastdeploy-hono
fastdeploy login
fastdeploygit init
git add .
git commit -m "initial hono + drizzle + sqlite api"- New repo on GitHub
- Do not add README or .gitignore
git branch -M main
git remote add origin https://github.com/YOURNAME/beans-cool-api.git
git push -u origin mainOnce you are done making your own project, https://forms.hackclub.com/haxmas-day-4 your project! Have fun!