Quick Start
Getting a Worker reading and writing through these adapters, from nothing.
1. Declare the environment
Extend the package's contract with whatever your Worker adds of its own:
// src/types/Env.ts
import type { CloudflareEnv } from '@bayudwiyansatria/cloudflare'
export interface Env extends CloudflareEnv {
LOG_LEVEL?: string
}
CloudflareEnv carries the bindings — KV, DB, BUCKET, QUEUE and the rest — every one of them optional, because
a library cannot know which resources you provisioned.
2. Register the configuration surface
The kernel holds the runtime-neutral settings; this package contributes the binding-backed half. The application joins them:
// src/config/index.ts
import { configure, systemDefaults } from '@bayudwiyansatria/core'
import { cloudflareDefaults } from '@bayudwiyansatria/cloudflare'
export const defaults = {
...systemDefaults,
...cloudflareDefaults,
logging: { ...systemDefaults.logging, ...cloudflareDefaults.logging }
}
export const overrides = {
kv: { expirationTtl: 60 },
logging: { service: 'example-worker' }
}
configure(defaults, overrides)
The halves are almost disjoint — the kernel owns security, this package owns the binding-backed modules — so a flat
spread carries all of those across untouched. logging is the one key both declare: the kernel sets the level, and this
package narrows which context fields reach the line (see cloudflareDefaults.logging).
Spreading alone would drop one whole block for the other, and a bundler that folds the two spreads into a single object
literal reports the collision as a Duplicate key "logging" warning on every wrangler dev and wrangler deploy.
Merging that one module by hand settles both: this package's narrower values win key by key, the kernel's survive
wherever they are not restated, and the literal names logging once.
Overrides apply per field, so naming expirationTtl keeps the default binding of KV.
Where to call configure()
Put it in the configuration module itself, as above, and have anything that reads configuration at startup import that module:
// src/app.ts
import '@/config' // must come first — see below
import { Hono } from 'hono'
ES imports are hoisted, so a configure() call written in your entry file runs after every module it imports has
already initialised. That matters because registering routes is startup work by nature: SecurityMiddleware.apply needs
the protected pattern at the moment it registers, not per request. Putting the call in config/ and naming that
dependency makes the module graph enforce the order, since a module is always fully evaluated before its importers.
Get this wrong and the Worker throws ConfigurationError on startup, with a message naming both likely causes.
3. Use a capability
// src/services/ArticleService.ts
import { Service } from '@bayudwiyansatria/core'
import { D1Service, KVService } from '@bayudwiyansatria/cloudflare'
import type { Env } from '@/types/Env'
const cache = new KVService()
const db = new D1Service()
export class ArticleService extends Service {
public async get(env: Env, id: number) {
const article = await cache.remember(env, `article:${id}`, () =>
db.first(env, 'SELECT * FROM articles WHERE id = ?', id)
)
return article ? this.ok('article found', article) : this.fail('article not found')
}
}
Accessors are created once at module scope and reused across requests. That is safe because an accessor holds the
binding's name, not the binding — the binding itself is read off the env passed to each call.
Degradation is part of the contract
A Worker deployed without a KV namespace still serves. cache.remember returns the loaded value, cache.get reads as a
miss, and writes are dropped — so an unprovisioned cache makes the Worker slower, never broken. Each capability
documents whether it degrades or throws, and isAvailable(env) reports the truth if you need to branch.
4. Add the middleware (optional)
// src/app.ts
import { logToAnalytics, SecurityMiddleware } from '@bayudwiyansatria/cloudflare/middlewares'
const app = new Hono()
app.use('*', logToAnalytics) // first, so rejected requests are still observed
new SecurityMiddleware().apply(app)
Order is load-bearing. SecurityMiddleware short-circuits a rejected request, so registering the logger ahead of it is
what keeps a 401 in both the logstream and the metrics. The other way round it leaves no trace at all.
5. Wire the bindings
Nothing above names a binding, because the configuration layer does:
// wrangler.json
{
"kv_namespaces": [{ "binding": "KV", "id": "…" }],
"d1_databases": [{ "binding": "DB", "database_id": "…" }],
"analytics_engine_datasets": [{ "binding": "TELEMETRY", "dataset": "telemetry" }]
}
Those binding values match cloudflareDefaults. To use different names, override them rather than renaming anything
in code:
configure(defaults, { kv: { binding: 'SESSIONS' }, d1: { binding: 'ARTICLES_DB' } })
Verify locally
In this library repository, run npm test and npm run build. In a consuming Worker, start the Cloudflare development
runtime with npx wrangler dev and call a route that uses the configured capability. A startup ConfigurationError
means step 2 is incomplete.
Next
- Bindings — all eighteen accessors and their
wrangler.jsonwiring - Directory Structure — the layers and the boundary between them