The vendor-neutral kernel shared by my Node/TypeScript services: capability interfaces, configuration resolution, structured logging, and the service response contract.
This package is the bottom layer everything else depends on. It provides four things: interfaces describing what an
application needs from its platform, a configuration engine that merges defaults with overrides, a structured logger,
and an abstract Service base that fixes the response shape.
It has no runtime dependencies and names no vendor, no runtime, and no HTTP framework. A cache is described by the
CacheStore interface; which cache you actually get is decided by a separate adapter package. Applications import the
interface, adapters implement it, and business logic written against CacheStore moves between platforms unchanged.
Published to the GitHub npm registry as @bayudwiyansatria/core, built by Rollup into CommonJS, ESM, UMD, and type
declaration artifacts under lib/.
| Category | Exports |
|---|---|
| Contracts | Service, APIResponse |
| Capabilities | Capability plus ten interfaces over it, and four Noop* implementations |
| Configuration | configure, resolve, systemDefaults, SystemConfiguration, Overrides, settings types |
| Observability | Logger, LogContext, LogLevel, LoggingSettings |
| Security | timingSafeEqual |
| Errors | MissingCapabilityError, ConfigurationError |
| Utilities | Signature, Text, Time |
Tooling and project baseline:
@/* path aliases (tsconfig*.json)lib/dist/coverage, reported to Codecovdist/docs, with undocumented exports treated as build failuresdist/book, with a custom dark themenpm install @bayudwiyansatria/core
The package publishes to the GitHub npm registry, so .npmrc needs:
@bayudwiyansatria:registry=https://npm.pkg.github.com
import { configure, Logger, systemDefaults } from '@bayudwiyansatria/core'
import { platformDefaults } from '<the-adapter-package>'
// Once, at startup, before anything reads a setting.
configure({ ...systemDefaults, ...platformDefaults }, { logging: { service: 'article-api' } })
const log = Logger.fromEnv(env, { service: 'ArticleService' })
log.info('ready')
Call configure() once at startup, then read settings anywhere with resolve().
import { configure, resolve, systemDefaults } from '@bayudwiyansatria/core'
import type { LoggingSettings } from '@bayudwiyansatria/core'
configure(systemDefaults, { logging: { service: 'article-api' } })
const logging = resolve<LoggingSettings>('logging')
// { level: 'info', service: 'article-api' }
Overrides merge per field, not per module. The example above changes service and keeps the default level. A field
set to undefined is ignored rather than treated as a blank.
resolve() throws a ConfigurationError when a module is in neither the defaults nor the overrides. In practice that
means either configure() has not run yet, or two copies of this package are loaded. See
Notes for Adapters.
Logger writes structured JSON lines. fromEnv reads LOG_LEVEL off the environment object you hand it and falls back
to the configured level. Levels are debug, info, warn, error.
import { Logger } from '@bayudwiyansatria/core'
const log = Logger.fromEnv(env, { service: 'ArticleService' })
log.info('article created', { id: 42 })
log.error('upstream failed', { status: 502 })
const scoped = log.child({ requestId }) // inherits context, adds its own
Service is an abstract base giving you ok() and fail(), so every service returns the same APIResponse shape.
import { Service } from '@bayudwiyansatria/core'
import type { APIResponse } from '@bayudwiyansatria/core'
export class ProfileService extends Service {
public async get(id: string): Promise<APIResponse> {
const profile = await this.repository.find(id)
return profile ? this.ok('profile found', profile) : this.fail('no profile with that id')
}
}
Use fail() for outcomes the caller asked for and did not get, like a missing row. Throw for faults the caller cannot
act on, so they surface as a 500 instead of a well-formed false.
Ten interfaces describe what an application needs from its platform. Each extends Capability and is generic over the
runtime type an adapter binds to.
| Interface | Covers |
|---|---|
CacheStore |
key/value cache |
CoordinationStore |
locks and coordination |
DataStore |
document/record storage |
InferenceEngine |
model inference |
MessageQueue |
publish and consume |
ObjectStore |
blob storage |
RateLimiter |
request throttling |
SqlConnectionProvider |
relational connections |
TelemetrySink |
metrics and request facts |
VectorIndex |
vector search |
NoopCacheStore, NoopMessageQueue, NoopRateLimiter, and NoopTelemetrySink satisfy their interface without doing
anything, which is useful in tests and in deployments where the capability is not wired up.
Signature.sign(secret, scope, value) and Signature.verify(...) for stateless tokens over HMAC-SHA-256Text.truncate, Text.trim, Text.isBlankTime.now, Time.nowIso, Time.sincetimingSafeEqual(a, b) for constant-time string comparisonsrc/
index.ts Public API barrel, the entire published surface
core/ Service, Logger, configure, resolve, ConfigurationRegistry
noop/ No-op capability implementations
types/ Shapes callers receive or supply
capabilities/ The capability interfaces and their supporting shapes
constants/ systemDefaults
exceptions/ MissingCapabilityError, ConfigurationError
security/ timingSafeEqual
utils/ Signature, Text, Time
test/ Jest specs (*.spec.ts) mirroring src/
docs/ Documentation and release history
reference/ Reference guides
styles/ Custom CSS for the HonKit book theme
book.json HonKit configuration
SUMMARY.md Book table of contents
changes-log/ Per-date change logs
release-notes/ Per-version release notes
docker/ Docker assets for the docs site
nginx/ Nginx config and landing page
docs.Dockerfile Multi-stage build for the docs site
docker-compose.docs.yaml
eslint.config.ts Layering and vendor-neutrality rules
rollup.config.ts Build pipeline for the four lib/ artifacts
CHANGELOG.md Top-level changelog
CONTRIBUTING.md Contribution workflow
SECURITY.md Security policy
SUPPORT.md Support channels
types/, constants/, exceptions/, security/, and utils/ are leaf layers: they import nothing else from src/.
Only core/ and index.ts import across layers. Keeping the leaves free of the config engine and the logger is what
lets them be read, tested, or moved elsewhere on their own.
A no-restricted-imports rule in eslint.config.ts enforces it, so this fails npm run lint:
// src/utils/Text.ts
import { resolve } from '@/core'
If a leaf genuinely needs something from core/, take it as an argument or move the shared piece down into a leaf.
security/ is separate from utils/ on intent rather than size. timingSafeEqual exists because the obvious
alternative is unsafe. JWT and PKI helpers would go there too.
npm run build Build the four lib/ artifacts via Rollupnpm run dev Rollup in watch modenpm test Run lint and testsnpm run test:run Run Jest with coveragenpm run lint:run Run ESLint over srcnpm run lint:fix Run ESLint with auto-fixnpm run format Run Prettier over config, src, and testnpm run build:docs Generate the TypeDoc API reference into dist/docsnpm run build:docs:book Build the HonKit developer book into dist/booknpm run build:docs:coverage Produce the coverage report into dist/coveragenpm run build:static Build every static artifact into dist/ (index.html, docs/, book/, coverage/)npm run build:all Build the library and the static sitenpm run docker:build:docs Build the docs site imagenpm run docker:serve:docs Serve the docs site with Docker ComposeIf code here looks like it needs a vendor SDK, a runtime's types, or an HTTP framework, it belongs in an adapter package instead. An ESLint rule enforces this.
Adapters must treat this package as an external dependency, not bundle it. The configuration registry is module state
and MissingCapabilityError is a class identity, so a second inlined copy breaks both quietly: configure() seeds one
registry while the adapter reads the other, and instanceof stops matching. Assert this against your built bundles
rather than trusting the bundler config.
npm install
npm test # lint + jest
npm run build # four artifacts under lib/
npm run build:docs
typedoc.json sets treatWarningsAsErrors and validation.notDocumented, so an undocumented export or a broken
cross-reference fails npm run build:docs.
CI runs on push to master
(Main) and on feature/** and
hotfix/** branches
(Features). Releases are
cut manually through the Release Dispatch workflow.
configure, resolve, merge semantics, and the ordering
constraintThe docs site combines three artifacts behind an nginx landing page:
dist/docsdist/coveragedist/book, with dark theme and full-text searchBuild them locally:
npm run build:static
Or serve the whole site with Docker:
npm run docker:serve:docs
Then open http://localhost for the landing page, which links to the docs, the coverage report, and the book.
Thanks to the open-source community and all contributors for support and inspiration.
MIT. See LICENSE.