@ikatec/frame
v0.2.0
Published
Reusable backend framework — Hono, Pino, OpenTelemetry, Drizzle/Sequelize, built-in DI. Runs on Node and Cloudflare Workers.
Keywords
Readme
@ikatec/frame
Reusable backend framework for Node and Cloudflare Workers, built from a single source tree. Hono routing, Pino logging, OpenTelemetry observability, Drizzle (and Sequelize on Node) persistence, a provider-agnostic mail/jobs/events layer, and a built-in, zero-dependency dependency-injection container. Pick the subpaths you need — most run on both runtimes; the few that pull a Node-only dependency are called out below.
Install
# base — always required
npm install @ikatec/frame hono pino @opentelemetry/apiEverything else is an optional peer, installed only for the features you use:
npm install @hono/zod-validator zod # @ikatec/frame/validation
npm install drizzle-orm # database/drizzle + repositories/drizzle (Node + Workers)
npm install sequelize sequelize-typescript # database/sequelize + repositories/sequelize (Node only)
npm install @hono/node-server # apps/node + apps/jobs (Node HTTP server)
npm install nodemailer # mail/smtp (Node only)
npm install @opentelemetry/sdk-node # observability/node-sdk (Node)
npm install @opentelemetry/sdk-trace-base @opentelemetry/exporter-trace-otlp-http # observability/workers-sdkThe AWS SES mail provider needs no extra install — @aws-sdk/client-sesv2 ships as a
direct dependency.
Requirements: Node >=22 <23. The DI decorators are TC39 legacy decorators —
enable them in your tsconfig.json (see TypeScript setup).
Packages
Each sub-package is its own import subpath (@ikatec/frame/<name>) with its own README:
apps— composition-root base classes (ApiApp;apps/nodeNodeApiApp,apps/workersWorkersApiApp, NodeJobsApp/ScriptsApp)cache— asyncCacheinterface + in-memory and Cloudflare KV implementationscls— continuation-local storage overAsyncLocalStorageconfig—BaseConfigcontract + DI tokencrypto— AES-256-CBC JSON ↔ URL-safe base64database—DatabaseServicecontract + Drizzle / Sequelize implsdi— DI container (Container,@Service,@Inject,Token)errors—HttpErrorclasses + serialize-error helpersevents— domainEventBusdispatching onto the job queuehttpClient— axios-based HTTP client with retriesjobs— background-job registry + HTTP / sync dispatcherslogs—Loggerinterface + Pino implementationmail— provider-agnostic email (MailService, SES, SMTP)middleware— Hono error / logging / observability middlewaremodules— type-onlyModuleManifestcomposition contractobservability— OpenTelemetry seam (withSpan, runtime drivers)repositories—Repository<T>interface + filter DSLscripts— named CLI script registrytransformers— type-onlyTransformer<T, U>contractuseCases—UseCasebase classvalidation—validate(zod-validator wrapper)
Internal helpers (not export subpaths): tests (setupTestDB), utils.
Runtime targets
Most subpaths are runtime-neutral — the same import works on Node and Cloudflare Workers. The exceptions each pull a runtime-specific dependency:
| Subpath | Runs on | Why |
| --- | --- | --- |
| @ikatec/frame/cache (InMemoryCache) | Node only | setInterval().unref() (KvCache, same subpath, is Workers-oriented) |
| @ikatec/frame/httpClient | Node only | axios + node http/https agents |
| @ikatec/frame/apps/node (NodeApiApp) | Node only | @hono/node-server (long-lived server) |
| @ikatec/frame/apps/jobs (JobsApp) | Node only | @hono/node-server |
| @ikatec/frame/apps/scripts (ScriptsApp) | Node only | yargs |
| @ikatec/frame/database/sequelize | Node only | sequelize |
| @ikatec/frame/repositories/sequelize | Node only | sequelize |
| @ikatec/frame/mail/smtp (SmtpEmailProvider) | Node only | nodemailer |
| @ikatec/frame/observability/node-sdk (NodeObservability) | Node only | @opentelemetry/sdk-node |
| @ikatec/frame/observability/workers-sdk (WorkersObservability) | Workers only | @opentelemetry/sdk-trace-base + OTLP/fetch |
The neutral counterparts (@ikatec/frame/apps's ApiApp, database/drizzle,
repositories/drizzle, mail's AwsSesEmailProvider, logs, observability's
interface, jobs/sync, validation, …) run on both.
Node-only subpaths are not gated behind an export condition, so importing one in a Workers bundle drags its Node-only dependency in. Import the neutral subpath (or the Workers driver) on Workers.
Dependency injection
A small, opinionated DI toolkit ships under @ikatec/frame/di: a
singleton Container, @Service / @Inject decorators, and Token for non-class
identifiers (config values, interfaces, primitives). Zero dependencies, no
reflect-metadata.
import { Container, Service, Inject } from '@ikatec/frame/di'
@Service()
class Logger {
log(msg: string) { console.log(msg) }
}
@Service()
class UserService {
@Inject(() => Logger) private logger!: Logger
greet() { this.logger.log('hello') }
}
Container.get(UserService).greet()Container.get(id)/Container.set(id, value)— resolve / registerContainer.has(id)/Container.remove(id)/Container.reset()@Service(options?)— mark a class injectable ({ transient: true }for per-resolve instances)@Inject(() => Class)— lazy property injection (supports forward refs); pass aTokendirectlyToken<T>(name)— opaque identifier for non-class deps
See the DI README for scopes, circular-dependency handling, and inheritance.
Observability per runtime
OpenTelemetry works on both runtimes via the neutral
@ikatec/frame/observability interface
(ObservabilityToken, withSpan). Register the driver that matches your runtime:
| Runtime | Driver | Backed by |
| --- | --- | --- |
| Node | @ikatec/frame/observability/node-sdk (NodeObservability) | @opentelemetry/sdk-node (auto-instrumentation) |
| Workers | @ikatec/frame/observability/workers-sdk (WorkersObservability) | @opentelemetry/sdk-trace-base + OTLP/HTTP-over-fetch |
// Cloudflare Worker
import { ObservabilityToken } from '@ikatec/frame/observability'
import { WorkersObservability } from '@ikatec/frame/observability/workers-sdk'
import { Container } from '@ikatec/frame/di'
const obs = new WorkersObservability({ endpoint: env.OTLP_ENDPOINT, headers: { /* ... */ } })
obs.start()
Container.set(ObservabilityToken, obs)
// flush before the isolate suspends — exports run on `fetch`, which the Worker cancels at response:
ctx.waitUntil(obs.forceFlush())WorkersObservability uses a SimpleSpanProcessor (not BatchSpanProcessor, whose
background timers don't fire between Worker invocations) and takes config explicitly
(there's no process.env on Workers). Cloudflare's own
automatic tracing can also
export OTLP with no code if you don't need custom spans.
TypeScript setup
The @Service / @Inject decorators are TC39 legacy decorators, and property
injection relies on declared-but-uninitialized fields staying absent until the
container wires them. Configure your tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"experimentalDecorators": true,
"useDefineForClassFields": false,
"emitDecoratorMetadata": false // not needed — no reflect-metadata
}
}Getting started
Subclass BaseConfig, pick a database implementation, register both in the
Container, and start an ApiApp. Working setups live in examples/:
| Example | Runtime | Shows |
| --- | --- | --- |
| minimal-node | Node | smallest NodeApiApp + listen() server |
| minimal-cloudflare | Workers | smallest Workers fetch handler |
| modular-node | Node | ModuleManifest composition over Drizzle/SQLite |
| production-fullstack | Workers | end-to-end Hono RPC + D1 + React, a production-shaped monorepo |
Releasing
CI lives in .gitlab-ci.yml: every branch/MR pipeline runs lint,
type-check, and tests, then builds dist/. The testcontainers-based Postgres
integration suites (*.integration.test.ts) are skipped in CI via
SKIP_INTEGRATION_TESTS=1 because the runner can't run docker-in-docker yet — run
them locally with a plain npm test.
Releases are automated with semantic-release
(config in .releaserc.json): every push to main analyzes the conventional-commit
history, and when it warrants a release, bumps the version, publishes to npm, pushes a
chore(release) commit + tag, and creates the GitLab release. No manual tagging or
version bumps — fix: → patch, feat: → minor, BREAKING CHANGE: → major.
Requires the NPM_TOKEN and GITLAB_TOKEN (api scope) CI/CD variables.
Status
0.1.0 — initial extraction from ikatec-backoffice.
