Don't worry, we're building something WAY simpler than a machine learning CRM!
Make sure you have bun installed!
You will also need:
- a GitHub account
- a ID verified Hack Club account
A tiny web framework. You define routes like this:
- “When someone does
GET /api/stuff, run this function.”
A database that's 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 don't have to do it 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 I'll 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, you do need at least 2 API routes! 🎅
This is the layout you're aiming for, if something later breaks check if your stuff looks like this!:
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-projectWhen it asks:
- Install project dependencies?
Yes(press Enter) - Which package manager?
bun(press Enter)
TLDR: yes, bun
Then run:
cd my-project
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 you open the URL Hono printed, your server works! Yipeeeee!
bun add drizzle-orm dotenv
bun add -D drizzle-kit @types/bun better-sqlite3What 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 (so you don't commit your secrets or DB!):
.env
my.db
🛑 STOP & THINK!
This is where you decide what your app is about. Think of schema as the database’s "types":
- table name (e.g.,
presents,elves,cookies) - column names
- column types
- default values
Create src/db/schema.ts.
You can copy this exactly if you want to make a "Wishlist" app, OR rename wishes to whatever you want!
import { sqliteTable, integer, text } from "drizzle-orm/sqlite-core"
// "wishes" is the table name in the DB
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(),
})Cheat Sheet for your own ideas:
Primary key (your table needs this)
id: integer("id").primaryKey({ autoIncrement: true }),Text / strings
text("name").notNull() // required
text("description") // optionalNumbers / booleans
integer("score").notNull()
integer("is_nice").notNull().default(0) // 0 = false, 1 = trueCreate 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
Tip: If you change your schema later (like adding 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 { Database } from "bun:sqlite"
import { drizzle } from "drizzle-orm/bun-sqlite"
const sqlite = new Database(process.env.DB_FILE_NAME!)
export const db = drizzle(sqlite)Routes should be readable. So we make helper functions.
If you changed your table name in Step 4, update these functions to match!
Create src/db/queries.ts:
import { db } from "./index"
import { wishes } from "./schema" // <--- Import YOUR table here
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,
}).returning({ id: wishes.id }).get()
return res
}
export function fulfillWish(id: number) {
const res = db.update(wishes)
.set({ fulfilled: 1 })
.where(eq(wishes.id, id))
.returning({ id: wishes.id })
.get()
return res
}
export function deleteWish(id: number) {
const res = db.delete(wishes)
.where(eq(wishes.id, id))
.returning({ id: wishes.id })
.get()
return res
}Now your routes become short and understandable.
In src/index.ts:
import { Hono } from "hono"
import { createWish, deleteWish, fulfillWish, listWishes } from "./db/queries"
const app = new Hono()
app.get("/", (c) => c.text("Beans!"))
// GET all wishes
app.get("/api/wishes", (c) => c.json(listWishes()))
// POST a new wish
app.post("/api/wishes", async (c) => {
const body = await c.req.json().catch(() => null)
// "item" matches the column name in our schema
const item = (body?.item ?? "").toString().trim()
if (!item) return c.json({ error: "item is required" }, 400)
return c.json(createWish(item), 201)
})
// PATCH (update) a wish
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) return c.json({ error: "not found" }, 404)
return c.json({ ok: true })
})
// DELETE a wish
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) return c.json({ error: "not found" }, 404)
return c.json({ ok: true })
})
export default appHere is how you can test your API!
Add a wish:
curl -X POST http://localhost:3000/api/wishes \
-H "content-type: application/json" \
-d '{"item":"lego"}'List wishes:
curl http://localhost:3000/api/wishesFulfill a wish:
curl -X PATCH http://localhost:3000/api/wishes/1/fulfillDelete a wish:
curl -X DELETE http://localhost:3000/api/wishes/1Hosting providers set the port for you. You must read process.env.PORT.
At the bottom of src/index.ts, replace the export default app with:
const port = Number(process.env.PORT) || 3000
console.log(`Server is running on port ${port}`)
export default {
port,
fetch: app.fetch,
}Let's put this on the internet! We'll use a tool just made for this workshop, usally you would use a server like nest... but thats gone so lets use this custom tool i made, it will only work for this workshop!
bun install -g fastdeploy-honofastdeploy login# Make sure your DB is up to date first
bunx drizzle-kit push
# Ship it!
fastdeployOnce it's done, it will give you a URL.
You can now use that URL instead of localhost:3000 in your curl commands!
Example:
curl 'https://fastdeploy.deployor.dev/u/ident!RLwfBZ/test12121/api/wishes'git init
git add .
git commit -m "initial hono + drizzle + sqlite api"- Go to GitHub and create a New Repository
- Name it whatever you want
- Do not add a README or .gitignore (we already have them)
Copy the commands GitHub gives you, which look like this:
git branch -M main
git remote add origin https://github.com/YOURNAME/YOUR-REPO.git
git push -u origin mainOnce you are done making your own project, take the URL that FastDeploy gave you and git url and submit it here: https://forms.hackclub.com/haxmas-day-4
Have fun and Hacky Holidays! 🎄

