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:
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)Recommended layout for medium projects#
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 typesApp 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
appdirectly fromapp.ts.
// Pure application — no runtime importsimport { 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)// Runtime entry — imports app + adapter onlyimport { 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.
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:
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.jsonThe 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:
// Both of these are equivalent:import { jwtAuth } from '@kynetra/fx'import { jwtAuth } from '@kynetra/fx-auth'Tip
@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:
// Cloudflare Workers environment bindingsinterface Env { DB: D1Database KV: KVNamespace R2: R2Bucket API_SECRET: string}Then pass the type parameter when reading env values:
app.get('/ping', (ctx) => { const env = ctx.env as Env return ctx.text(env.API_SECRET ? 'ok' : 'missing secret')})Note
d1Sql() and d1Store() adapters.