Skip to content
NewComposite primary keys5 min read

Hono

Hono is fetch-native, so it mounts the HTTP transport core directly: .mount() strips the prefix before the handler sees the request. Nothing to install beyond uql-orm.

import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { createFetchHandler } from 'uql-orm/http';
import { pool } from './uql.config.js';
import { Post, User } from './shared/models/index.js';
const handler = createFetchHandler({ pool, include: [User, Post] });
const app = new Hono();
app.use('*', cors());
app.get('/health', (c) => c.text('ok'));
app.post('/checkout', async (c) => c.json(await runCheckout(c.req.raw))); // custom business logic
app.mount('/api', handler); // entity CRUD under /api
export default app; // Bun and Workers; Deno.serve(app.fetch), or serve(app) from @hono/node-server

That serves the full wire protocol per entity, QUERY included, since .mount() forwards every method. It claims only the /api prefix, so your own routes and middleware sit beside the generated CRUD; keep them for read-modify-write logic, multi-entity transactions, aggregations, uploads and streaming. Unknown routes under the prefix 404 from the handler rather than falling through.

The mount is optional. pool is a plain ORM, so any route can query it:

app.get('/posts', async (c) => {
const posts = await pool.findMany(Post, {
$where: { published: true },
$limit: 20,
});
return c.json(posts);
});

Work spanning several statements goes in pool.transaction, which hands you the querier to run all of them on.

Nothing here is Hono-specific, so the core documents it once: getContext takes the web Request and scopes every query in it through a security filter, and the hooks shape a response, stamp a field or reject a payload. A hook that throws with a numeric status becomes that HTTP status.

The same handler runs unchanged on Bun, Deno and Node. D1 is the exception: its binding arrives with the request, and .mount() hands the handler a bare Request with no way back to c.env. Route it yourself and pass the prefix as basePath:

import { Hono } from 'hono';
import { type D1Database, D1QuerierPool } from 'uql-orm/d1';
import { createFetchHandler } from 'uql-orm/http';
import { Post, User } from './shared/models/index.js';
const app = new Hono<{ Bindings: { DB: D1Database } }>();
app.all('/api/*', (c) => {
const handler = createFetchHandler({
pool: new D1QuerierPool(c.env.DB),
include: [User, Post],
basePath: '/api',
});
return handler(c.req.raw);
});

D1 has no transactions, so its write routes cannot run; reads and single-statement writes do.

Hono’s own RPC client infers from chained routes, so hc cannot see mounted CRUD: the typed client for it is UQL’s browser client, which sends the same query you write on the server. For per-procedure contracts, see tRPC and oRPC.