Module Pattern
Apply to every module in klex. No exceptions.
File structure
Each module lives in its own folder. Two files are required:
- Named entry file — same name as the folder. Contains all implementation (class, factory, interfaces, types).
index.tsbarrel — re-exports all public symbols from the entry file. This is what consumers import from.
config/
config.ts # implementation: ConfigModule, createConfig, Config interface
config.test.ts # tests import from './config'
types.ts # internal type definitions (not re-exported unless public)
index.ts # export * from './config'; export * from './types';
- Entry file name = folder name.
config/config.ts, notconfig/index.ts. index.tsonly re-exports. No logic, no implementation.- Tests sit next to the entry file:
config.test.ts, notindex.test.ts. - Internal helpers within a module keep their own descriptive names (
server.ts,migrate.ts).
Import paths
- Cross-module imports use the
@/alias + barrel:from '@/config',from '@/model-provider'. - Intra-module imports stay relative:
from './server',from './types'. - Never bubble up relative paths (
../../config) — use@/instead.
// Good — cross-module via alias + barrel
import { createConfig } from '@/config';
import type { ModelProvider } from '@/model-provider';
// Good — intra-module relative
import { createAdminApp } from './server';
import type { Config } from './config'; // within same module folder
Infrastructure
The @/ alias requires configuration in three places — all must stay in sync:
tsconfig.json:paths: { "@/*": ["./src/*"] }. NobaseUrl(TS 7+ removed it).build.ts(esbuild):alias: { "@/": "./src/" }.vitest.config.ts:resolve.alias: { "@/": <src path> }.
Rules
Implement each module as a class.
Instantiate modules only through a factory function. Never
newoutside the factory.Factory returns the module's public handle — the interface consumers use.
export interface AgentRuntime {
start(): Promise<void>;
run(input: AgentInput): Promise<AgentResult>;
close(): Promise<void>;
}
class AgentRuntimeModule implements AgentRuntime {
// implementation
}
export function createAgentRuntime(
deps: AgentRuntimeDependencies,
): AgentRuntime {
return new AgentRuntimeModule(deps);
}
Keep implementation classes private unless direct construction or extension is explicitly supported.
Constructors only store configuration and dependencies. Async initialization belongs in
start().Lifecycle-managed modules expose idempotent
start()andclose(). Callingstart()twice = no-op. Callingclose()beforestart()= no-op.A module owns and closes only the child modules and resources it creates. Injected dependencies are borrowed — never close them.
Start owned children in dependency order. Close them in reverse order.
Clean up already-started children when startup fails. Partial startup → rollback.
Keep dependencies explicit. No mutable global singletons.
Module folder name = entry file name.
admin-api/admin-api.ts,router/router.ts. Entry file holds all implementation.index.tsbarrel re-exports only public symbols. No logic. Consumers import from the barrel (@/config), never directly from the entry file across modules.