Kynetra FX

Documentation

Node.js

Run Kynetra FX on Node.js using the @kynetra/fx-node adapter. The adapter bridges the Web Fetch API that Kynetra FX uses internally to the node:http module — so the same application code runs unchanged on Cloudflare Workers, Bun, and Deno.

Overview#

Kynetra FX routes are written against the standard Web Fetch API: app.fetch(request: Request): Promise<Response>. This is the same interface that Cloudflare Workers, Bun, and Deno expose natively. On Node.js, the @kynetra/fx-node adapter translates between the Node IncomingMessage / ServerResponse types and the platform-agnostic Fetch types so you do not have to.

  • Zero lock-in — swap the adapter out and the same app instance runs elsewhere.
  • Supports streaming responses via the Web Streams API.
  • Works with all Kynetra FX middleware, plugins, and typed routes.
  • Compatible with Node.js 18 and later (where the global Request / Response are available).

Tip

If you are deploying to Cloudflare Workers you do not need this adapter. See the Cloudflare Workers page instead.

Installation#

Install the core framework and the Node.js adapter together. The adapter is a separate package so it does not add any Node-specific dependencies to deployments that run on other runtimes.

terminal
# npm
npm install @kynetra/fx @kynetra/fx-node
 
# pnpm
pnpm add @kynetra/fx @kynetra/fx-node
 
# bun
bun add @kynetra/fx @kynetra/fx-node

Quick start with serve()#

The serve() export from @kynetra/fx-node creates an http.Server internally and calls app.fetch for each incoming request. It returns the underlying http.Server instance if you need to attach WebSocket upgrade handlers or other low-level listeners.

1

Create your application file

Import createFX and serve, define your routes, and call serve with the app and options.
2

Set the runtime hint

Pass { runtime: 'node' } to createFX. This hint lets the framework pick sensible defaults for Node — such as disabling the cf bindings lookup that is only available on Cloudflare.
3

Start the server

Run node server.ts (or tsx server.ts for TypeScript without a build step). The onListen callback fires once the port is bound.
server.ts
import { createFX } from '@kynetra/fx'
import { serve } from '@kynetra/fx-node'
 
const app = createFX({ runtime: 'node' })
 
app.get('/', (ctx) => ctx.json({ hello: 'world' }))
app.get('/health', (ctx) => ctx.json({ status: 'ok' }))
 
serve(app, { port: 3000 })

How app.fetch works#

app.fetch is a plain (Request) => Promise<Response> function. Every runtime that supports the Web Fetch API can call it directly without any adapter — Bun and Deno do this out of the box. Node.js is the only runtime where an explicit bridge is needed, which is exactly what @kynetra/fx-node provides.

This means you can write your entire application once, put it in a shared file, and import it from runtime-specific entry points. The example below shows the same app object wired to four different runtimes without any changes to the application logic.

app.ts (shared)
// Same app object — no changes needed between runtimes
const app = createFX()
 
app.get('/api/hello', (ctx) => ctx.json({ hello: 'world' }))
 
// Node.js via @kynetra/fx-node
import { serve } from '@kynetra/fx-node'
serve(app, { port: 3000 })
 
// Bun — export directly
return { port: 3000, fetch: app.fetch.bind(app) }
 
// Deno
Deno.serve({ port: 3000 }, app.fetch.bind(app))
 
// Cloudflare Workers
return { fetch: app.fetch.bind(app) }

Note

The .bind(app) call is necessary when passing app.fetch as a bare function reference, because fetch internally reads from this. Alternatively you can write (req) => app.fetch(req) as a wrapper.

Manual node:http integration#

If you need full control over the HTTP server — for example to configure TLS, set socket timeouts, or share the server with other handlers — you can construct a node:http server manually and call app.fetch inside the request callback.

server-manual.ts
import http from 'node:http'
import { createFX } from '@kynetra/fx'
 
const app = createFX()
 
app.get('/health', (ctx) => ctx.json({ status: 'ok' }))
 
const server = http.createServer(async (req, res) => {
const url = `http://${req.headers.host}${req.url}`
const request = new Request(url, {
method: req.method,
headers: req.headers as HeadersInit,
})
const response = await app.fetch(request)
res.writeHead(response.status, Object.fromEntries(response.headers))
res.end(await response.text())
})
 
server.listen(3000, () => console.log('Listening on :3000'))

The key steps are: reconstruct the full URL from the request headers, create a Web Request object, call app.fetch, and write the Response back to the Node response object. This pattern also lets you integrate Kynetra FX alongside legacy Express or Fastify handlers in the same process during a migration.

Environment variables and production setup#

In production you will typically read PORT, HOST, and other configuration from environment variables. You can also compose middleware before calling serve. Here is a production-ready entry point with logging, secure headers, and CORS configured from environment variables:

server.ts
import { createFX, logger, secureHeaders, cors } from '@kynetra/fx'
import { serve } from '@kynetra/fx-node'
 
const app = createFX({ runtime: 'node' })
 
// Middleware
app.use(logger())
app.use(secureHeaders())
app.use(cors({ origin: process.env.ALLOWED_ORIGIN ?? '*' }))
 
// Routes
app.get('/', (ctx) => ctx.json({ ok: true }))
 
const port = parseInt(process.env.PORT ?? '3000', 10)
const hostname = process.env.HOST ?? '0.0.0.0'
 
serve(app, {
port,
hostname,
onListen({ port, hostname }) {
console.log(`Server running at http://${hostname}:${port}`)
},
})

The onListen callback receives the resolved port and hostname — useful when the OS picks an ephemeral port (pass port: 0) in tests.

Warning

Do not pass secrets through process.env into route handlers at module load time — read them inside the handler or pass them via the context so they are accessible across runtimes that may not have a process global.

serve() options#

The second argument to serve() accepts the following options:

NameTypeDescription
portnumberTCP port to listen on. Defaults to 3000. Pass 0 to let the OS assign an ephemeral port.
hostnamestringNetwork interface to bind to. Defaults to '0.0.0.0' (all interfaces). Use '127.0.0.1' to restrict to loopback.
onListen(info: { port: number; hostname: string }) => voidCallback invoked once the server is bound and ready to accept connections.

See also#