Free & open source — no account required
Free & open source — no account required
This technical guide provides an in-depth analysis of the json to hono engine, best practices for implementation, and data security standards.
Hono is a small, fast web framework built for edge runtimes — Cloudflare Workers, Deno Deploy, Bun, and Node.js. Its TypeScript support is first-class: route types flow through the entire request lifecycle, and the RPC client (hc) lets your frontend call the backend with full end-to-end type inference. Generating Zod schemas from your JSON gives you the typed validators that Hono needs to make this work.
Hono uses TypeScript generics to thread types through routes, middleware, and the RPC client. The key insight is that app.route() and hc() can infer the full API shape — request and response — if you define your validators correctly:
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
const app = new Hono();
const CreateProductSchema = z.object({
name: z.string().min(1).max(100),
price_cents: z.number().int().nonnegative(),
sku: z.string().min(1),
});
const route = app.post(
'/products',
zValidator('json', CreateProductSchema),
async (c) => {
// c.req.valid('json') is typed as z.infer<typeof CreateProductSchema> ✅
const data = c.req.valid('json');
const product = await db.products.create(data);
return c.json(product, 201);
}
);
// Export the route type for the RPC client
export type AppType = typeof route;
import { z } from 'zod';
import { zValidator } from '@hono/zod-validator';
const ListProductsQuery = z.object({
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().min(1).max(100).default(20),
category: z.string().optional(),
in_stock: z.coerce.boolean().optional(),
});
const ProductParamsSchema = z.object({
id: z.string().uuid(),
});
const productsRouter = new Hono()
.get('/',
zValidator('query', ListProductsQuery),
async (c) => {
// query params are strings from the URL — z.coerce.number() handles the conversion
const { page, limit, category } = c.req.valid('query');
const products = await db.products.list({ page, limit, category });
return c.json({ items: products, page, limit });
}
)
.get('/:id',
zValidator('param', ProductParamsSchema),
async (c) => {
const { id } = c.req.valid('param');
const product = await db.products.findById(id);
if (!product) return c.notFound();
return c.json(product);
}
);
Hono's c.set / c.get supports typed context variables via generics — useful for auth middleware:
import { createMiddleware } from 'hono/factory';
// Define the context variable types
type Env = {
Variables: {
user: { id: string; role: 'admin' | 'user' };
};
};
// Auth middleware that sets the typed variable
const authMiddleware = createMiddleware<Env>(async (c, next) => {
const token = c.req.header('Authorization')?.split(' ')[1];
if (!token) return c.json({ error: 'Unauthorized' }, 401);
const user = await verifyToken(token); // returns { id, role }
c.set('user', user);
await next();
});
const protectedRouter = new Hono<Env>()
.use('*', authMiddleware)
.get('/me', (c) => {
const user = c.get('user'); // ✅ typed as { id: string; role: 'admin' | 'user' }
return c.json(user);
});
Hono's hc client reads the exported app type and provides typed fetch calls — no manual API client needed:
// server/routes/products.ts
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
const CreateProductSchema = z.object({
name: z.string().min(1),
price_cents: z.number().int().nonnegative(),
});
export const productsApp = new Hono()
.post('/',
zValidator('json', CreateProductSchema),
async (c) => {
const data = c.req.valid('json');
const product = await db.products.create(data);
return c.json(product, 201);
}
);
// Export the app type for the RPC client
export type ProductsAppType = typeof productsApp;
// --- client/api.ts ---
import { hc } from 'hono/client';
import type { ProductsAppType } from '../server/routes/products';
const client = hc<ProductsAppType>('http://localhost:3000');
// Fully typed — TypeScript knows the request body and response shape
const response = await client.index.$post({
json: { name: 'Widget', price_cents: 2999 },
});
const product = await response.json(); // ✅ typed
// src/index.ts — Cloudflare Workers entry point
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
// Env bindings — D1 database, KV, etc.
type Bindings = {
DB: D1Database;
CACHE: KVNamespace;
API_KEY: string;
};
const app = new Hono<{ Bindings: Bindings }>();
const IngestSchema = z.object({
event: z.string(),
payload: z.record(z.string(), z.unknown()),
ts: z.number().int(),
});
app.post('/ingest',
zValidator('json', IngestSchema),
async (c) => {
// c.env is typed as Bindings ✅
const { DB, API_KEY } = c.env;
const apiKey = c.req.header('x-api-key');
if (apiKey !== API_KEY) return c.json({ error: 'Forbidden' }, 403);
const event = c.req.valid('json');
await DB.prepare('INSERT INTO events (event, payload, ts) VALUES (?, ?, ?)')
.bind(event.event, JSON.stringify(event.payload), event.ts)
.run();
return c.json({ ok: true });
}
);
export default app;
z.coerce.number() and z.coerce.boolean() (not z.number()) for numeric and boolean query params — otherwise Zod will reject "5" as non-numeric.hc client only infers types correctly when routes are chained on the same instance: app.get(...).post(...). Calling app.get() and app.post() as separate statements on const app loses the chained type.typeof app) to infer request/response shapes. Exporting the app instance alone doesn't include the type — export a named AppType or inline the type annotation.Q: How does Hono compare to Express for TypeScript projects?
A: Hono has first-class TypeScript support with no @types/ package needed, runs on edge runtimes, and has built-in RPC type inference. Express works well on Node.js but wasn't designed for TypeScript or edge. For new projects targeting Workers, Bun, or Deno, Hono is the default choice.
Q: Can I use Hono on Node.js, not just edge runtimes?
A: Yes. import { serve } from '@hono/node-server' runs Hono on Node.js with the same code. Most Hono code is runtime-agnostic — exceptions are things like using native Node.js APIs, which aren't available on Workers.
Q: How do I handle file uploads in Hono?
A: Use the built-in parseBody for multipart data: const body = await c.req.parseBody(). This returns a plain object where file fields are File objects. Validate the non-file fields separately with zValidator on other body params.
Q: Does zValidator return a 400 automatically on validation failure?
A: Yes, by default it returns a 400 JSON response with the Zod error. You can customize the error handler by passing a callback as the third argument: zValidator('json', schema, (result, c) => { if (!result.success) return c.json({ errors: result.error.flatten() }, 422) }).
Is the processing local-only?
Absolutely. TypeMorph operates entirely within your browser's sandbox. We use Web Workers for high-performance computation without ever transmitting your JSON, SQL, or API data to a remote server.
Can I use this for enterprise projects?
Yes. The tool is designed for professional software engineers who require GDPR compliance and data privacy. It is trusted by developers at top-tier startups and financial institutions.