Kynetra FX

Documentation

Project Structure

Kynetra FX does not impose a directory structure. This page describes the layout that scales well from a small side project to a large production monorepo.

Single-file apps#

For small projects or edge functions, everything in one file is perfectly fine:

src/index.ts
import { createFX, logger } from '@kynetra/fx'
import { cloudflare } from '@kynetra/fx-cloudflare'
 
const app = createFX()
app.use(logger())
app.get('/', (ctx) => ctx.text('hello'))
 
return cloudflare(app)
project layout
src/
app.ts // createFX(), middleware, route registration
index.ts // runtime entry point (exports for Cloudflare / starts server)
routes/
users.ts // app.group('/users', ...) handlers
auth.ts
health.ts
middleware/
auth.ts // custom auth middleware
tenant.ts
lib/
db.ts // port adapters (d1Store, d1Sql, etc.)
errors.ts // domain-specific FXError helpers
types/
env.d.ts // Cloudflare Env binding types

App vs adapter separation#

The most important architectural decision in a Kynetra FX project is keeping the application (business logic, routes, middleware) separate from the runtime adapter (the export that ties it to Cloudflare, Node, or Bun). This separation means:

  • You can unit-test the app with app.fetch(new Request(...)) without a real runtime.
  • You can swap the runtime adapter without touching your routes.
  • Integration tests can import app directly from app.ts.
src/app.ts
// Pure application — no runtime imports
import { createFX, requestId, logger, cors } from '@kynetra/fx'
import { userRoutes } from './routes/users'
import { authRoutes } from './routes/auth'
 
export const app = createFX({ name: 'my-api' })
 
app.use(requestId())
app.use(logger())
app.use(cors())
 
userRoutes(app)
authRoutes(app)
src/index.ts
// Runtime entry — imports app + adapter only
import { cloudflare } from '@kynetra/fx-cloudflare'
import { app } from './app'
 
return cloudflare(app)

Route modules#

Keep related routes together in modules that accept the app (or a group) as a parameter. This keeps route registration co-located with the handlers and avoids circular dependencies.

src/routes/users.ts
import type { FXApp } from '@kynetra/fx'
 
export function userRoutes(app: FXApp) {
app.group('/users', (g) => {
g.get('/', listUsers)
g.post('/', createUser)
g.get('/:id', getUser)
g.put('/:id', updateUser)
g.delete('/:id', deleteUser)
})
}
 
async function listUsers(ctx: any) {
return ctx.json([])
}
 
async function createUser(ctx: any) {
return ctx.json({}, { status: 201 })
}
 
async function getUser(ctx: any) {
return ctx.json({ id: ctx.params.id })
}
 
async function updateUser(ctx: any) {
return ctx.json({ id: ctx.params.id })
}
 
async function deleteUser(ctx: any) {
return ctx.json(null, { status: 204 })
}

Monorepo layout#

For larger projects that share types between the API and a frontend, a monorepo structure works well:

monorepo layout
packages/
api/
src/
app.ts
index.ts
routes/
package.json
web/
app/ // Next.js or similar
package.json
shared/
src/
types.ts // Shared TypeScript types
client.ts // Generated or hand-written typed client
package.json

The typed client (createFXClient) can live in the shared package, consuming the same contract metadata exported from the API. See Typed Client for details.

Umbrella vs subpackages#

You can import everything from @kynetra/fx (the umbrella package) or from individual subpackages like @kynetra/fx-auth. The umbrella re-exports all subpackage APIs, so both work identically at runtime:

src/app.ts
// Both of these are equivalent:
import { jwtAuth } from '@kynetra/fx'
import { jwtAuth } from '@kynetra/fx-auth'

Tip

Use the umbrella @kynetra/fx import in application code. Use subpackage imports in library code that should not pull in the full framework as a dependency.

Environment types (Cloudflare)#

For Cloudflare Workers, declare your bindings in a .d.ts file so TypeScript knows what is on ctx.env:

src/types/env.d.ts
// Cloudflare Workers environment bindings
interface Env {
DB: D1Database
KV: KVNamespace
R2: R2Bucket
API_SECRET: string
}

Then pass the type parameter when reading env values:

src/routes/users.ts
app.get('/ping', (ctx) => {
const env = ctx.env as Env
return ctx.text(env.API_SECRET ? 'ok' : 'missing secret')
})

Note

See Cloudflare adapter for the full D1 and KV integration guide, including the d1Sql() and d1Store() adapters.