HonoSupported
Hono is a small, fast, standards-based router. @zmdb/web's WebApplication exposes fetch(request) and handle(req) and owns no server, which makes the two compose rather than compete — you can mount zmdb inside Hono, or use zmdb's data layer under Hono routes and skip @zmdb/web entirely.
zmdb data layer, Hono routes#
The lightest combination, and a good default if you already like Hono:
import { Hono } from 'hono';
import { assert } from '@zmdb/validator';
import { type CreateDTO } from '@zmdb/orm';
const api = new Hono();
api.get('/posts', async c => c.json(await postRepo.list({ page: { limit: 20 } })));
api.post('/posts', async c => {
const dto = assert<CreateDTO<Post>>(await c.req.json());
return c.json(await postRepo.create(dto), 201);
});No decorators, no container, no @zmdb/web at all. The schema, compiler, repository and validators are independent of the web package.
Mount @zmdb/web inside Hono#
Because WebApplication.fetch takes and returns web-standard Request/Response, it mounts as a Hono handler:
const zmdbApp = createApp(AppModule);
await zmdbApp.init();
const hono = new Hono();
hono.all('/api/*', c => zmdbApp.fetch(c.req.raw));
hono.get('/health', c => c.text('ok'));Use this when you want zmdb's DI and OpenAPI for the API surface and Hono's middleware for everything around it — CORS, static assets, compression, streaming.
Your controllers see the full path, so a controller behind /api/* must be declared as @Controller('/api/posts'). WebApplication does not know it is mounted and does not strip a prefix.
Hono middleware fills real gaps#
This is the strongest argument for the combination. Several cross-cutting features @zmdb/web does not ship are one Hono middleware away:
Gap in @zmdb/web | Hono |
|---|---|
| Compression | compress() |
| Static files | serveStatic() |
| CSRF | csrf() |
| Rate limiting, CORS, secure headers | middleware |
Put those outside the mount, where Hono owns the Response. For an application-owned response stream, @zmdb/web itself provides stream().
Choosing between them#
| Hono | @zmdb/web | |
|---|---|---|
| Runtime dependencies | hono | zero |
| Style | functional handlers | classes + decorators |
| DI container | none | built-in |
| Modules | none | module graph |
| OpenAPI | via @hono/zod-openapi | native |
| Streaming | yes | no |
| Middleware ecosystem | large | none |
If your application is a handful of routes, Hono plus zmdb's data layer is less machinery. If it has services, layered dependencies and a generated API contract, @zmdb/web earns its structure.
Validation#
assert<T> works as a Hono validator with no adapter:
api.post('/posts', async c => {
const dto = assert<CreateDTO<Post>>(await c.req.json());
// ...
});Catch validation failures in app.onError and map them to a 400. Without the transformer, the generic call instead throws runtime type witness required in test/fallback mode, so keep a build-path canary:
it('the transformer is running', () => {
expect(is<{ id: number }>({ id: 'x' })).toBe(false);
});On the edge#
Hono targets Workers, Deno and Bun; so does zmdb's read path, with an HTTP driver. Bun still needs a separate build route. React Native uses the withZmdb Metro wrapper. Keep the canary against either built artefact.
---
See also: Standalone Applications · Streaming · Serverless Performance