I was really excited when I came across Hono. I think the API is elegant in its simplicity, and I’ve found it—in my admittedly limited experience—to be a sturdy foundation for moderately complicated backends.
In short, Hono is fast, flexible, and honestly fun to work with. Templates will get you started in a dozen different runtimes and frameworks, and there are a multitude of plugins and middleware to facilitate integration with third-party tools.
How do all of these pieces fit together though? While project constraints and implementation details will vary, most data APIs need to satisfy three key requirements:
- A way to persist queryable data, typically a database,
- To define and regulate how data moves between application layers,
- And to safeguard against malicious activity and user error.
When you’re just getting started with a framework, ostensibly simple steps like configuring the database or spinning up a validation layer can become grueling hurdles to adoption. Enter as a template for non-trivial Hono APIs.
0 to 60 with the HONC app
HONC is more of a design philosophy than a rigid doctrine. You can use the create-honc-app CLI to download a project with either a Neon, D1, or Supabase DB. Drizzle plays a pivotal role by managing seeding and migrations, and decoupling the stack from the database. As the source of truth for (DB) type definitions, it’s can also be the foundation of your type system and runtime validation.
This gets us from 0 to 60, but what about the 80/20, or at least 70/30? Implementation details like validation layers and rate limiting are too contingent on business requirements to usefully include in a template, but when you pick up a library for the first time, having a robust examples is a game-changer.
Mocking a (moderately) advanced data API
To simulate what happens when a design philosophy collides with project constraints, Fiberplane asked me to build a simple mock-data API called ) using the is an API testing and debugging tool—like the Inspector panel in your browser—that we’ll be using to inspect requests, logs, and database calls.
A mock-data API’s functional requirements are robust enough to involve all key aspects of API development, but not so complicated as to be distracting. At a bare minimum, they serve relational data via multiple application layers, but they can be usefully enhanced with features like validation and rate limiting.
To give ourselves some concrete parameters, we settled on a handful of features that most production-ready data APIs must implement:
- A database with an ORM or custom adapter layer
- Validation and business logic
- Error handling and rate limiting
- and a lightweight markdown-based frontend for docs
This is the first article in a series that will cover 1) building the app, 2) deploying to production, and 3) rendering a front end. We hope the series has something to offer more- and less-experienced devs alike, but we’ll be focused on patterns, helpers, and gotchas, so we won’t be explaining basic data API or TypeScript patterns.
Getting up and running with HONC
To get started, we’ll download the , you’ll need to update the D1 section in your wrangler.toml (the Cloudflare Worker config file). We’ll cover this in detail in the next article. For now, take a moment to get acquainted with the config, and update the database name and ID to match your project.
[[d1_databases]]
binding = "DB"
database_name = "placegoose-d1"
# Can be anything for local development
# Must be updated when connecting to a remote DB
database_id = "local-placegoose-d1"
migrations_dir = "drizzle/migrations"
The binding value is the key used to access the database from within the app. If you choose to rename it, be sure to keep the Bindings property of AppType in sync for proper intellisense and type propagation.
type AppType = {
Bindings: {
// Global type from @cloudflare/workers-types
DB: D1Database;
}
};
// Any instances connecting to the DB must be typed
const app = new Hono<AppType>();
If you’re new to (or ambivalent about) TypeScript, don’t worry: Despite this being a fully-integrated TypeScript project, AppType is one of the only types you’ll need to define and manage yourself! In fact, this is it for project setup, so why don’t we take a look at the HONC stack’s lynchpin: Drizzle ORM.
Type-safe database management with Drizzle ORM
As I alluded to earlier, Drizzle does a lot of heavy lifting for us. In any project with a database, we need to manage table schemas and migrations, bridge the gap between JavaScript and SQL syntax, and validate data going into the DB.
That’s a non-trivial task, especially for a small team or solo dev, and demands a lot of discipline to build and maintain. Drizzle offers all of this in a type-safe package that lets us derive types and validation models directly from table definitions.
This schema-first approach is meant to ensure that updates are reflected across the stack, meaning fewer files to update and fewer migration bugs.
Defining a single source of truth
The HONC template comes with a single table definition (db/schema.ts) that demonstrates how to require a column, default a value, and run raw SQL.
By default, Drizzle names columns after the keys in your table definitions. For seamless translation between camel and snake case, take advantage of Drizzle’s uses your table models to programmatically generate seed data and populate the database.
To create the seed data,
Like most mainstream HTTP clients you can “replay” requests, making it a piece of cake to rapidly test defined happy and sad paths after refactoring an endpoint. By integrating logs and more robust traces though, I found that Fiberplane cut down on some of the back-and-forth between my HTTP client and my terminal.
Having this comprehensive insight into the request lifecycle built into my HTTP client was helpful throughout development, but especially when building in more complex features like validation, and when trying to optimize handler performance.
Querying the bound databases
With telemetry set up, we’re ready to start querying and serving data! First, we need to connect to the database by calling the
drizzleinitializer, which expects a D1 client bound to the app. This is where theBindingsproperty onAppTypecomes in. Hono exposes bindings and other environment values through theContextobject, whose typing is inherited from its immediate parent.
In the source code I abstract the call in order to reduce repetition, but the following examples will show it inline for clarity. Though tempting, I chose not to use a singleton because there didn’t seem to be much benefit for such a simple service and short-lived service.
The initializer also accepts an optional
configargument. Since we’re making use of Drizzle’s auto-casing, we need to specify that the client should expect snake case from the DB.
CODEimport { drizzle } from "drizzle-orm/d1";
import { Hono } from "hono";
type AppType = {
Bindings: {
// Global type we get from @cloudflare/workers-types
DB: D1Database;
}
};
const gagglesApp = new Hono<AppType>();
// Get all Gaggles
gagglesApp.get("/", async (c) => {
// We get our DB binding from Context
const db = drizzle(c.env.DB, {
// This must be set for Drizzle to automatically
// translate between snake and camel case
casing: "snake_case"
});
// Drizzle inference tells us is type Gaggle[]
const gaggles = await db
.select()
.from(schema.gaggles);
return c.json(gaggles);
});
export default gagglesApp;
Drizzle ORM aims to be a lightweight abstraction over SQL, so query construction is fairly intuitive. Statements are represented as chains of keywords, and Drizzle exports operators as flavor-specific helper methods.
Enforcing Drizzle types at run-time
To keep compile- and run-time types in sync, we’ll create a validation layer using the
drizzle-zodplugin. It gives us constructors that build Zod schemas from Drizzle table models. As with the types exposed on table models, there is an Insert and a Select option.
CODEimport { createInsertSchema } from "drizzle-zod";
import * as schema from "./schema";
// src/db/validation.ts
export const ZGaggleInsert = createInsertSchema(schema.gaggles, {
name: (schema) => schema.name.min(1),
territory: (schema) => schema.territory.min(1),
});
Initially I was worried this might be limiting, but Drizzle makes it easy to extend or override field definitions. Zod accepts empty string values by default, so I made use of this feature to require that name fields were at least populated.
Zod is an awesome schema library you can use to keep your types and validation in sync. I won’t be discussing how to use the library here, but I encourage you to check out the so that it would be more useful for consumers. In retrospect, I should have called this in the body validator factory, and included the results in a , an open source learning resource for session-based auth. They provide great guidelines, and examples for most common frameworks.
There was a lot I couldn’t cover in this article, but I hope that I’ve highlighted how the HONC stack can be used to address key requirements for lightweight data APIs, namely persistence, data integrity, and system security. Its minimal footprint helps it leverage performance on the edge, while its schema-first approach to typing streamlines system stability and maintainability.
Above all, the HONC stack is a strong but flexible framework, into which we can easily integrate important features like validation and rate limiting without losing type safety.
In the next article, @brettimus from Fiberplane will cover deploying Placegoose to production, including how to seed a remote D1. To conclude the series, we’ll discuss using markdown to render API docs with a custom layout.
↗ Original-Artikel auf dev.to lesenVollständiger Original-BerichtAusführliche Details, Code-Beispiele & Hersteller-Stellungnahme auf dev.to.
SOCIAL SHARE CARD GENERATOR