Kynetra FX

Documentation

Migrating from Hono

Kynetra FX and Hono share the same Web Standards mental model — both are built on Request, Response, Headers, URL, and fetch. If you know Hono, you already understand the shape of Kynetra FX. The differences are naming conventions, how groups work, and how validation is wired up. Kynetra FX does not wrap or depend on Hono — there is no Hono import anywhere in the runtime.

Concept Mapping#

The table below maps every common Hono API to its Kynetra FX equivalent. Most are one-to-one renames; a few reflect intentional design differences.

NameTypeDescription
new Hono()createFX({ name, runtime })Constructor replaced by factory with explicit config.
app.route("/path", sub)app.group("/path", fn)Sub-app mounting is replaced by the group builder API.
c (context arg)ctx (context arg)Same shape, different variable name.
c.json(data)ctx.json(data, init?)Return instead of call; accepts a ResponseInit second arg.
c.text(s)ctx.text(s)Plain-text response helper.
c.html(s)ctx.html(s)HTML response helper.
c.redirect(url)ctx.redirect(url, status?)Optional status code (default 302).
c.req.param("id")ctx.params.idParams is a plain object, not a method.
c.req.query("q")ctx.query("q")Method call; no args returns all params as a record.
c.req.header("x-foo")ctx.header("x-foo")Reads a request header.
c.req.json()ctx.jsonBody<T>()Explicit async body read; can only be read once.
c.req.text()ctx.textBody()Reads the body as a plain string.
c.req.formData()ctx.formBody()Reads the body as FormData.
c.set("key", val)ctx.decorate("key", val)Sets a per-request context decoration.
c.get("key")ctx.get("key")Reads a context decoration.
c.status(code)ctx.status(code)Chainable status setter.
c.header("X-Foo", val) (response)ctx.set("X-Foo", val)Sets a response header. ctx.header() reads request headers only.
Hono middleware (c, next)(ctx, next) => next()Identical onion model; always await next().
zValidator("json", schema)app.route({ input: schema, handler })Validation is first-class in the route contract.
app.onError(fn)app.hook('onError', fn)Error hook; return a Response to override the default.
return appreturn cloudflare(app)Must be wrapped in the runtime adapter for Workers.

App Setup#

Hono is instantiated with new Hono() and exported directly. Kynetra FX uses a factory function that requires a name and runtime, and the export is wrapped in the runtime adapter so Cloudflare Workers receives the right handler shape.

before.ts
import { Hono } from 'hono'
const app = new Hono()
return app
after.ts
import { createFX, cloudflare } from '@kynetra/fx'
 
const app = createFX({ name: 'api', runtime: 'cloudflare' })
return cloudflare(app)

Basic Routes and Params#

Route methods are identical (app.get, app.post, etc.). The only changes are the context variable name and how params and query strings are accessed.ctx.params is a plain object, not a method — use dot notation directly.

before.ts
app.get('/users/:id', (c) => {
const id = c.req.param('id')
return c.json({ id })
})
 
app.get('/search', (c) => {
const q = c.req.query('q')
return c.json({ q })
})
after.ts
app.get('/users/:id', (ctx) => {
return ctx.json({ id: ctx.params.id })
})
 
app.get('/search', (ctx) => {
return ctx.json({ q: ctx.query('q') })
})

Wildcard routes use * and are matched last (static > dynamic > wildcard).ctx.query() with no argument returns all query parameters as a Record<string, string>.

Middleware#

Middleware is structurally identical: both frameworks use the onion model where calling await next() proceeds to the next layer and code after it runs on the way back out. The context variable is renamed from c to ctx. Kynetra FX ships four built-in middleware helpers you can use directly.

before.ts
app.use('*', async (c, next) => {
console.log('before', c.req.url)
await next()
console.log('after')
})
after.ts
import { logger, requestId, secureHeaders, cors } from '@kynetra/fx'
 
// Built-in middleware
app.use(logger())
app.use(requestId())
app.use(secureHeaders())
app.use(cors())
 
// Custom middleware
app.use(async (ctx, next) => {
console.log('before', ctx.url.href)
await next()
console.log('after')
})

Per-route middleware is passed as additional arguments before the handler: app.get('/path', mw1, mw2, handler).

Body Handling#

Body reading is explicit in Kynetra FX — call the appropriate method rather than accessing a pre-parsed property. The Request.body stream can only be consumed once, so Kynetra FX never reads it speculatively.

before.ts
app.post('/items', async (c) => {
const body = await c.req.json()
return c.json({ created: body }, 201)
})
after.ts
app.post('/items', async (ctx) => {
const body = await ctx.jsonBody()
return ctx.json({ created: body }, { status: 201 })
})

Route Groups#

Hono uses app.route(path, sub) to mount a separate Hono instance. Kynetra FX uses app.group(path, fn) with a scoped builder. This is important: in Kynetra FX, app.route() is for typed contracts with validation, not sub-app mounting.

before.ts
const api = new Hono()
api.get('/users', (c) => c.json([]))
api.post('/users', (c) => c.json({}, 201))
app.route('/api', api)
after.ts
app.group('/api', (api) => {
api.get('/users', (ctx) => ctx.json([]))
api.post('/users', (ctx) => ctx.json({}, { status: 201 }))
})

Groups support middleware scoped to the prefix. Any group.use() call applies only within that group and nested sub-groups.

after.ts
import { requireAuth } from '@kynetra/fx-auth'
 
app.group('/admin', (admin) => {
admin.use(requireAuth([jwtStrategy]))
admin.get('/stats', (ctx) => ctx.json({ ok: true }))
admin.group('/users', (users) => {
users.get('/', (ctx) => ctx.json([]))
})
})

Validation#

Hono relies on third-party validators like @hono/zod-validator. Kynetra FX has first-class contract support via app.route(). The validated body is available as the typed ctx.input property — no manual parsing needed. You can use the built-in fx schema builder or any Standard Schema–compatible library such as Zod or Valibot.

before.ts
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
 
app.post(
'/issues',
zValidator('json', z.object({ title: z.string().min(1) })),
(c) => {
const data = c.req.valid('json')
return c.json(data, 201)
}
)
after.ts
import { createFX, fx } from '@kynetra/fx'
 
// Using the built-in fx schema builder
app.route({
method: 'POST',
path: '/issues',
input: fx.object({ title: fx.string().min(1) }),
handler: (ctx) => ctx.json(ctx.input, { status: 201 }),
})
 
// Or with Zod — Standard Schema compatible, no adapter needed
import { z } from 'zod'
 
app.route({
method: 'POST',
path: '/issues',
input: z.object({ title: z.string().min(1) }),
handler: (ctx) => ctx.json(ctx.input, { status: 201 }),
})

Validation failures return a 422 with an FX_VALIDATION_ERROR body before your handler runs. Query string validation works the same way via the query field on the contract.

Error Handling#

Hono exposes app.onError as a top-level method. In Kynetra FX, error handling is part of the unified hook system. The hook receives (err, ctx) — return aResponse to override the default error body, or return nothing to let Kynetra FX emit its standard structured error.

before.ts
app.onError((err, c) => {
return c.json({ error: err.message }, 500)
})
after.ts
import { FXError } from '@kynetra/fx'
 
app.hook('onError', (err, ctx) => {
if (err instanceof FXError) {
return ctx.json({ error: err.message }, { status: err.status })
}
return ctx.json({ error: 'Internal server error' }, { status: 500 })
})

Deploy / Entry Point#

On Cloudflare Workers, Hono apps are exported directly because Hono implements the Workers handler interface itself. Kynetra FX apps are wrapped with cloudflare(app), which returns the { fetch(request, env, ctx) } object Workers expects. On Node.js, use the serve adapter from @kynetra/fx-node.

before.ts
// Cloudflare Workers — Hono
import { Hono } from 'hono'
const app = new Hono()
// ...routes...
return app
after.ts
// Cloudflare Workers — Kynetra FX
import { createFX, cloudflare } from '@kynetra/fx'
 
const app = createFX({ name: 'api', runtime: 'cloudflare' })
// ...routes...
return cloudflare(app)
 
// --- Node.js ---
// import { createFX } from '@kynetra/fx'
// import { serve } from '@kynetra/fx-node'
// const app = createFX({ name: 'api', runtime: 'node' })
// serve(app, { port: 3000 })

Warning

Do not confuse app.route() with Hono's app.route(path, sub). In Kynetra FX, app.route() defines a typed contract with optional input/output validation — it is not for mounting sub-applications. Use app.group(path, fn) to scope routes under a prefix. Mixing these up is the single most common migration mistake.

Migration Checklist#

  • Replace new Hono() with createFX({ name, runtime })
  • Replace c with ctx throughout all handlers and middleware
  • Replace c.req.param('id') with ctx.params.id
  • Replace c.req.query('q') with ctx.query('q')
  • Replace await c.req.json() with await ctx.jsonBody()
  • Replace c.req.text() with await ctx.textBody()
  • Replace c.req.formData() with await ctx.formBody()
  • Replace c.header('X-Foo', val) (response) with ctx.set('X-Foo', val)
  • Replace c.req.header('X-Foo') with ctx.header('X-Foo')
  • Replace c.set('key', val) with ctx.decorate('key', val)
  • Replace c.get('key') with ctx.get('key')
  • Replace app.route('/path', sub) with app.group('/path', fn)
  • Replace zValidator middleware with app.route({ input, handler })
  • Replace app.onError(fn) with app.hook('onError', fn)
  • Replace return app with return cloudflare(app)

Further Reading

See Routing for the full route API, Contracts for typed validation, Groups for prefix scoping, and Cloudflare for the Workers adapter.