Add a Hono Endpoint¶
The Hono REST template (and Full-Stack / Monorepo variants) ship a users CRUD. This recipe walks through adding a new endpoint end-to-end: route handler, Drizzle storage, Zod validation, and OpenAPI docs.
What You Get Out of the Box¶
The generated project already wires:
src/Server.res— Hono app with logger,/openapi.json,/docs(Scalar UI)src/Routes/Users.res— GET/POST/PUT/DELETE for/userssrc/Schema.res— Drizzleuserstablesrc/Db.res— libsql client + common query helperssrc/ZodOpenapi.res— Zod bindings for@hono/zod-openapi
Our goal: add a /posts endpoint that lists posts and creates one.
Step 1 — Add the Table to Schema.res¶
let posts = sqliteTable("posts", {
"id": intCol("id", {"primaryKey": true, "autoIncrement": true}),
"title": textCol("title", {"notNull": true}),
"body": textCol("body", {"notNull": true}),
"authorId": intCol("author_id", {"notNull": true}),
})
Generate and apply the migration:
pnpm db:generate
pnpm db:migrate
Step 2 — Write the Route Module¶
Create src/Routes/Posts.res:
let register = app => {
app->Hono.get("/posts", async ctx => {
let rows =
await Db.db
->Db.select({
"id": Schema.posts["id"],
"title": Schema.posts["title"],
"body": Schema.posts["body"],
"authorId": Schema.posts["authorId"],
})
->Db.from(Schema.posts)
->Db.allAsync
ctx->Hono.json(rows)
})
app->Hono.post("/posts", async ctx => {
let payload = await ctx->Hono.req->Hono.jsonBody
let inserted =
await Db.db
->Db.insert(Schema.posts)
->Db.values({
"title": payload["title"],
"body": payload["body"],
"authorId": payload["authorId"],
})
->Db.returning
ctx->Hono.status(201)->Hono.json(inserted->Array.get(0))
})
}
Step 3 — Register in Server.res¶
Routes.Users.register(app)
Routes.Posts.register(app)
Step 4 — Add Zod Validation¶
Lift the payload shape to a shared Zod schema:
let createPostSchema = ZodOpenapi.object({
"title": ZodOpenapi.string(~minLength=1, ()),
"body": ZodOpenapi.string(),
"authorId": ZodOpenapi.number(),
})
Validate before hitting the database:
app->Hono.post("/posts", async ctx => {
let raw = await ctx->Hono.req->Hono.jsonBody
switch createPostSchema->ZodOpenapi.safeParse(raw) {
| {success: true, data} =>
/* insert data */
| {success: false, error} =>
ctx->Hono.status(400)->Hono.json({"error": error})
}
})
Step 5 — Expose in OpenAPI¶
The template already wires /openapi.json and /docs. Routes registered via app->Hono.get(...) appear automatically. For richer metadata (tags, response schemas), switch to @hono/zod-openapi’s createRoute helper — see the Hono docs for the full pattern.
Step 6 — Test¶
# Terminal 1
pnpm dev
# Terminal 2
curl -X POST http://localhost:3000/posts \
-H 'Content-Type: application/json' \
-d '{"title":"Hello","body":"World","authorId":1}'
curl http://localhost:3000/posts
Open http://localhost:3000/docs to exercise the new endpoint in Scalar UI.
Common Pitfalls¶
Forgetting to run migrations —
db:generatecreates the SQL file,db:migrateapplies it. If your insert errors with “no such table”, you missed step 2.Mixing records and dicts — Drizzle’s row shape is a dict (accessed with
payload["title"]). Convert to a ReScript record at the boundary if you want stricter typing downstream.CORS during frontend development — use the Vite+ dev proxy (Full-Stack / Monorepo templates set this up automatically) instead of configuring CORS on Hono.