DEV Community

Gift Egwuenu
Gift Egwuenu

Posted on

Build a Bookmark Manager with the HONC Stack

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Configure Cloudflare D1

Let's create a new D1 database:

npx wrangler d1 create bookmarks_db
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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)`)
});
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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/htmx.org@1.9.5"></script>
      </head>
      <body class="bg-gray-100 min-h-screen">
        <h1 class="text-4xl text-center font-bold mb-4 my-6">📌 Bookmark Manager</h1>
        <div class="max-w-xl w-full mx-auto bg-white p-6 my-6 rounded shadow-md">
          <form
            hx-post="/api/bookmark"
            hx-target="#bookmarkList"
            hx-swap="beforeend"
            class="flex flex-col gap-2 mb-4"
          >
            <input
              type="text"
              name="title"
              placeholder="Title"
              required
              class="border p-2 rounded"
            />
            <input
              type="url"
              name="url"
              placeholder="URL"
              required
              class="border p-2 rounded"
            />
            {/* Description and Tags (optional) */}
            <input
              type="text"
              name="description"
              placeholder="Description"
              class="border p-2 rounded"
            />
            <input
              type="text"
              name="tags"
              placeholder="Tags (comma-separated)"
              class="border p-2 rounded"
            />
            <button
              type="submit"
              class="bg-blue-600 text-white py-2 px-4 rounded hover:bg-blue-700"
            >
              Add Bookmark
            </button>
          </form>

        </div>
        <ul id="bookmarkList" class="space-y-2 max-w-xl w-full mx-auto">
            {bookmarks.map((b) => (
              <li class="p-2 border rounded flex justify-between items-center bg-white" 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="bg-red-500 text-white px-2 py-1 rounded hover:bg-red-600"
                  >
                    Delete
                  </button>
                </div>
              </li>
            ))}
          </ul>
      </body>
    </html>
  );
});
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

Finally, deploy to production:

npm run deploy
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (2)

Collapse
 
brettimus profile image
Boots

love seeing honc apps pop up! do you have your app deployed somewhere?

Collapse
 
lauragift21 profile image
Gift Egwuenu • Edited