skip to content
Gift Egwuenu
Search articles
Back to articles

Build a Bookmark Manager with the HONC Stack!

· 6 min read

What Even is the Best Stack? 🤔

As developers, we are constantly in pursuit of the best way to build applications that are frictionless, scalable, and a joy to work with. The ecosystem is filled with countless frameworks, libraries, and tools, each promising to make development easier. But what if there was a stack that combined performance, simplicity, and flexibility?

Enter the HONC Stack.

What is the HONC Stack?

HONC is a modern full-stack development approach optimized for speed, developer experience, and global scalability. It stands for:

  • HHono: A lightweight, fast, and Edge-first web framework for building APIs and applications.
  • ODrizzle ORM: A type-safe ORM designed for SQL databases with a great developer experience.
  • NName your Database: Whether it’s Cloudflare D1 (SQLite on the Edge), Neon (serverless Postgres), or any other preferred database, HONC allows for flexibility.
  • CCloudflare: A powerful developer platform offering Workers, KV, R2, D1, and more, making it an ideal environment for deploying modern apps at the Edge.

Why the HONC Stack?

The HONC stack is designed to take advantage of modern cloud and Edge computing principles, enabling developers to:

  • ⚡ Build fast, globally distributed applications with Cloudflare Workers and Hono.
  • 🛡️ Ensure type safety and maintainability with Drizzle ORM.
  • 🗃️ Use a flexible database solution depending on the use case.
  • 🚀 Deploy effortlessly with Cloudflare’s robust global infrastructure.

Getting Started with HONC

Want to try the HONC stack for yourself? Setting up a new project is as easy as running:

npm create honc-app@latest

This command sets up a new application with Hono, Drizzle ORM, and Cloudflare Worker bindings pre-configured. During setup, you’ll be prompted to choose a template. Select the D1 base template to ensure your application is optimized for Cloudflare D1 as the database solution.

Build a Bookmark Manager with the Honc Stack

To showcase the HONC Stack in action, let’s build a simple Bookmark Manager that allows users to:

  • Add bookmarks (title, URL, description, tags)
  • View saved bookmarks
  • Delete bookmarks

Set Up Your HONC App

This part was already done in the step above, go ahead and run the application using:

npm run db:setup
npm run dev

Configure Cloudflare D1

Let’s create a new D1 database:

npx wrangler d1 create bookmarks_db

Add it to wrangler.toml and update the database name in package.json scripts accordingly:

[[d1_databases]]
binding = "DB"
database_name = "bookmarks_db"
database_id = "<your-database-id>"
migrations_dir = "drizzle/migrations"

Define the Database Schema (Drizzle ORM)

Update the schema.ts to define the bookmarks table:

import { sql } from "drizzle-orm";
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
export type NewBookmark = typeof bookmarks.$inferInsert;

export const bookmarks = sqliteTable("bookmarks", {
	id: integer("id", { mode: "number" }).primaryKey(),
	title: text("title").notNull(),
	url: text("url").notNull(),
	description: text("description"),
	tags: text("tags"),
	createdAt: text("created_at")
		.notNull()
		.default(sql`(CURRENT_TIMESTAMP)`),
});

Create API Routes with OpenAPI (Hono)

Next, update the index.ts to define API endpoints with OpenAPI support:

import { drizzle, type DrizzleD1Database } from "drizzle-orm/d1";
import { eq } from "drizzle-orm";
import { createFiberplane } from "@fiberplane/hono";
import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi";
import * as schema from "./db/schema";

type Bindings = {
	DB: D1Database;
};

type Variables = {
	db: DrizzleD1Database;
};

const app = new OpenAPIHono<{ Bindings: Bindings; Variables: Variables }>();

app.use(async (c, next) => {
	const db = drizzle(c.env.DB);
	c.set("db", db);
	await next();
});

const BookmarkSchema = z
	.object({
		id: z.number().openapi({ example: 1 }),
		title: z.string().openapi({ example: "My Bookmark" }),
		url: z.string().url().openapi({ example: "https://example.com" }),
		description: z.string().optional().openapi({ example: "A useful link" }),
		tags: z.string().optional().openapi({ example: "tech, coding" }),
	})
	.openapi("Bookmark");

const getBookmarks = createRoute({
	method: "get",
	path: "/api/bookmarks",
	responses: {
		200: {
			content: { "application/json": { schema: z.array(BookmarkSchema) } },
			description: "Bookmarks fetched successfully",
		},
	},
});

const createBookmark = createRoute({
	method: "post",
	path: "/api/bookmark",
	request: {
		body: {
			required: true,
			content: {
				"application/json": {
					schema: BookmarkSchema,
				},
			},
		},
	},
	responses: {
		201: {
			content: {
				"application/json": {
					schema: BookmarkSchema,
				},
			},
			description: "Bookmark created successfully",
		},
	},
});

const deleteBookmark = createRoute({
	method: "delete",
	path: "/api/bookmark/{id}",
	responses: {
		200: {
			content: {
				"application/json": { schema: z.object({ message: z.string() }) },
			},
			description: "Bookmark deleted successfully",
		},
	},
});

app.openapi(getBookmarks, async (c) => {
	const db = c.get("db");
	const bookmarks = await db.select().from(schema.bookmarks);
	return c.json(bookmarks);
});

app.openapi(createBookmark, async (c) => {
	const db = c.get("db");
	const { title, url, description, tags } = c.req.valid("json");
	const [newBookmark] = await db
		.insert(schema.bookmarks)
		.values({ title, url, description, tags })
		.returning();
	return c.json(newBookmark, 201);
});

export default app;

Seed the Database

To populate the database with actual sample bookmarks, update the existing scripts/seed.ts file to include:

import { bookmarks } from './src/db/schema';
...
const sampleBookmarks = [
  {
    title: "Hono Framework",
    url: "https://hono.dev",
    description: "A lightweight web framework for building APIs and applications.",
    tags: "hono, framework, edge",
  },
  {
    title: "Drizzle ORM",
    url: "https://orm.drizzle.team",
    description: "A type-safe ORM designed for SQL databases.",
    tags: "orm, database, typescript",
  },
  {
    title: "Cloudflare D1",
    url: "https://developers.cloudflare.com/d1/",
    description: "Cloudflare’s globally distributed, serverless database.",
    tags: "cloudflare, database, d1",
  },
  {
    title: "HTMX",
    url: "https://htmx.org",
    description: "A library that allows access to modern browser features directly from HTML.",
    tags: "htmx, frontend, html",
  },
  {
    title: "MDN Web Docs",
    url: "https://developer.mozilla.org",
    description: "Comprehensive documentation for web technologies.",
    tags: "documentation, web, mdn",
  },
];

seedDatabase();

async function seedDatabase() {
  ...
  try {
    await db.insert(bookmarks).values(sampleBookmarks);
    console.log('✅ Database seeded successfully!');
    if (!isProd) {
    }
  } catch (error) {
    console.error('❌ Error seeding database:', error);
    process.exit(1);
  } finally {
    process.exit(0);
  }
}

Build a Frontend with Hono JSX Renderer

Use Hono’s JSX renderer to serve the frontend dynamically:

import { jsxRenderer } from "hono/jsx-renderer";

app.use("*", jsxRenderer());

app.get("/", async (c) => {
	const db = c.get("db");
	const bookmarks: Bookmark[] = await db.select().from(schema.bookmarks);

	return c.render(
		<html>
			<head>
				<title>Bookmark Manager</title>
				<script src="https://cdn.tailwindcss.com"></script>
				<script src="https://unpkg.com/[email protected]"></script>
			</head>
			<body class="min-h-screen bg-gray-100">
				<h1 class="my-6 mb-4 text-center text-4xl font-bold">📌 Bookmark Manager</h1>
				<div class="mx-auto my-6 w-full max-w-xl rounded bg-white p-6 shadow-md">
					<form
						hx-post="/api/bookmark"
						hx-target="#bookmarkList"
						hx-swap="beforeend"
						class="mb-4 flex flex-col gap-2"
					>
						<input
							type="text"
							name="title"
							placeholder="Title"
							required
							class="rounded border p-2"
						/>
						<input type="url" name="url" placeholder="URL" required class="rounded border p-2" />
						{/* Description and Tags (optional) */}
						<input
							type="text"
							name="description"
							placeholder="Description"
							class="rounded border p-2"
						/>
						<input
							type="text"
							name="tags"
							placeholder="Tags (comma-separated)"
							class="rounded border p-2"
						/>
						<button
							type="submit"
							class="rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
						>
							Add Bookmark
						</button>
					</form>
				</div>
				<ul id="bookmarkList" class="mx-auto w-full max-w-xl space-y-2">
					{bookmarks.map((b) => (
						<li
							class="flex items-center justify-between rounded border bg-white p-2"
							id={`bookmark-${b.id}`}
						>
							<div class="flex flex-col">
								<span class="font-semibold">{b.title}</span>
								<small>{b.description}</small>
								<small class="text-gray-500">{b.tags ?? ""}</small>
							</div>
							<div class="space-x-2">
								<a href={b.url} target="_blank" class="text-blue-600 hover:underline">
									Visit
								</a>
								<button
									hx-delete={`/api/bookmark/${b.id}`}
									hx-target={`#bookmark-${b.id}`}
									hx-swap="outerHTML"
									class="rounded bg-red-500 px-2 py-1 text-white hover:bg-red-600"
								>
									Delete
								</button>
							</div>
						</li>
					))}
				</ul>
			</body>
		</html>,
	);
});

To serve the frontend with JSX, rename your entry files to .tsx (for example, index.tsx) instead of .ts, ensuring that Hono can properly compile and serve JSX. For the full code checkout the GitHub Repo.

Deploy Everything to Cloudflare

Afterwards, run the migration script for production and optionally seed the database:

npm run db:migrate:prod
npm run db:seed:prod

Finally, deploy to production:

npm run deploy

The Bookmark Manager is now live with a minimal frontend!

Wrapping Up

The HONC stack makes it easy to build modern, efficient applications. Try it out and start building today!