@roostjs/core
Dependency injection container, configuration management, middleware pipeline, application lifecycle, and service provider base class.
Installation
bun add @roostjs/coreRoostContainer API
RoostContainer is a lightweight dependency injection container. Tokens are class
constructors or symbols. Bindings are factories that receive the container as their first argument.
singleton<T>(token: Token<T>, factory: Factory<T>): void
Register a singleton binding. The factory is called once on first resolution. The same
instance is returned on every subsequent resolve() call for the same token.
bind<T>(token: Token<T>, factory: Factory<T>): void
Register a transient binding. The factory is called on every resolve() call,
returning a new instance each time.
resolve<T>(token: Token<T>): T
Resolve a dependency. Throws BindingNotFoundError if no binding is registered
for the token.
has(token: Token<unknown>): boolean
Returns true if a binding exists for the token. Searches parent containers
in the scope chain.
scoped(): Container
Create a child container that inherits all bindings from the parent but maintains its own singleton registry. Used for request-level isolation.
ConfigManager API
ConfigManager provides dot-notation access to nested configuration objects.
constructor(config?: Record<string, unknown>)
Create a new ConfigManager with optional initial configuration data.
get<T>(key: string, defaultValue?: T): T
Retrieve a value using dot-notation key (e.g., 'database.default').
Throws ConfigKeyNotFoundError if the key does not exist and no default is provided.
set(key: string, value: unknown): void
Set a configuration value. Creates intermediate objects for nested keys as needed.
has(key: string): boolean
Returns true if the dot-notation key exists in the configuration.
mergeEnv(env: Record<string, string | undefined>): void
Merge environment variables into configuration. Variable names are lowercased and underscores converted to dots before matching against existing keys. Only merges if the key already exists in the configuration.
// APP_DEBUG=true merges into app.debug
// APP_NAME=x is ignored if app.name was not in the initial config
config.mergeEnv({ APP_DEBUG: 'true' });Pipeline API
Pipeline orchestrates ordered middleware execution. Each middleware
receives a Request and a next function that calls
the remaining middleware chain.
use(middleware: Middleware | MiddlewareClass, ...args: string[]): this
Add middleware to the pipeline. Supports plain functions and class-based middleware.
Additional string arguments are passed to class middleware constructors.
Returns this for chaining.
handle(request: Request, destination: Handler): Promise<Response>
Run the request through all registered middleware in registration order, then call the
final destination handler.
withContainer(container: Container): this
Associate a container with the pipeline. Required for class-based middleware that needs dependency injection.
Application API
Application is the root orchestrator: it holds the container, config,
service providers, and global middleware pipeline.
static create(env: Record<string, unknown>, config?: Record<string, unknown>): Application
Create a new Application instance with Cloudflare Worker environment bindings and optional configuration.
register(Provider: ServiceProviderClass): this
Register a service provider. Providers are instantiated and their register()
method is called during boot(). Returns this for chaining.
useMiddleware(middleware: Middleware | MiddlewareClass, ...args: string[]): this
Add global middleware that runs on every request handled by this application.
onDispatch(handler: Handler): this
Set the request dispatcher called after all middleware. Replaces any previously set dispatcher.
async boot(): Promise<void>
Instantiate all registered service providers and call their register() then
boot() methods in registration order. Called automatically on the first
handle() invocation.
async handle(request: Request): Promise<Response>
Handle an incoming HTTP request. Creates a scoped container per request, runs global middleware, and calls the dispatcher.
ServiceProvider API
ServiceProvider is an abstract base class for bootstrapping application
features. Extend it to register bindings and run initialization logic.
abstract register(): Promise<void> | void
Register bindings into the container. Called before any boot() methods run,
so all providers have the opportunity to register before any boots.
boot(): Promise<void> | void
Optional. Runs after all providers have called register(). Safe to resolve
dependencies registered by other providers here.
app: Application
The application instance. Available in both register() and boot(). Access the container via this.app.container.
Types
type Token<T = unknown> = (abstract new (...args: any[]) => T) | string | symbol;
type Factory<T> = (container: Container) => T;
type Handler = (request: Request) => Promise<Response>;
interface Middleware {
handle(request: Request, next: Handler, ...args: string[]): Promise<Response>;
}
type MiddlewareClass = new (...args: any[]) => Middleware;Application.defer()
defer(promise: Promise<unknown>): void
Register a promise as background work to complete after the response is sent. Delegates
to ExecutionContext.waitUntil(). The Worker runtime will not terminate the isolate
until all deferred promises have settled.
app.onDispatch(async (request) => {
const result = await processRequest(request);
// Fire-and-forget: analytics write completes after the response is returned
app.defer(analytics.record({ path: new URL(request.url).pathname }));
return Response.json(result);
});defer() is a no-op when no ExecutionContext is present (e.g. in tests that call
app.handle(request) directly without a second argument).
RoostExecutionContext
interface RoostExecutionContext {
waitUntil(promise: Promise<unknown>): void;
passThroughOnException(): void;
}The execution context passed to Application.handle() as its second argument. Matches
the shape of the Cloudflare Worker ExecutionContext — pass it through directly from the
Worker fetch handler.
export default {
fetch(request: Request, env: Env, ctx: ExecutionContext) {
return app.handle(request, ctx);
},
};Logger API
Logger emits structured JSON log entries to console.log. Every entry carries a
requestId trace identifier so log lines from the same request can be correlated.
constructor(context: LogContext)
interface LogContext {
requestId: string;
method: string;
path: string;
userId?: string;
}info(message: string, data?: Record<string, unknown>): void
warn(message: string, data?: Record<string, unknown>): void
error(message: string, data?: Record<string, unknown>): void
debug(message: string, data?: Record<string, unknown>): void
Each method emits a LogEntry as a single-line JSON object. The optional data map is
merged into the entry under the data key.
interface LogEntry extends LogContext {
level: 'debug' | 'info' | 'warn' | 'error';
message: string;
timestamp: string; // ISO 8601
data?: Record<string, unknown>;
}static fake(context?: Partial<LogContext>): FakeLogger
Create a FakeLogger pre-filled with defaults for test use. See FakeLogger API below.
FakeLogger API
FakeLogger extends Logger but captures entries in memory instead of writing to
console.log. Used in tests to assert on logged output.
readonly entries: LogEntry[]
All log entries recorded since construction or the last restore() call.
assertLogged(level: LogLevel, message: string): void
Throws if no entry matching level with a message containing message exists in
entries. Useful as an assertion in unit tests.
assertNotLogged(level: LogLevel): void
Throws if any entry at the given level exists in entries.
restore(): void
Clear all recorded entries. Call between tests.
const logger = Logger.fake();
await handler(fakeRequest, logger);
logger.assertLogged('info', 'payment.processed');
logger.assertNotLogged('error');
logger.restore();RequestIdMiddleware
Extracts or generates a request trace ID and binds a configured Logger instance into
the request-scoped container.
import { RequestIdMiddleware } from '@roostjs/core/middleware/request-id';Behavior
- Reads
cf-rayfrom request headers; falls back tocrypto.randomUUID(). - Constructs a
Loggerwith{ requestId, method, path }. - Binds the logger into the request-scoped container under
Logger. - Sets
X-Request-Idon the outgoing response.
import { Application } from '@roostjs/core';
import { RequestIdMiddleware } from '@roostjs/core/middleware/request-id';
const app = Application.create(env);
app.useMiddleware(RequestIdMiddleware);Downstream handlers and middleware can then resolve a pre-configured Logger from the
scoped container.
Webhook Verification API
verifyWebhook(request: Request, options: WebhookVerifyOptions): Promise<string>
Verify an incoming webhook's HMAC or Ed25519 signature. Returns the raw request body as
a string on success. Throws WebhookVerificationError on any failure (missing header,
invalid signature, expired timestamp).
interface WebhookVerifyOptions {
secret: string | Uint8Array;
headerName: string;
algorithm: 'hmac-sha256' | 'hmac-sha512' | 'ed25519';
timestampHeader?: string;
parseTimestamp?: (headerValue: string) => number;
parseSignature?: (headerValue: string) => string;
buildSignedPayload?: (timestamp: number | null, body: string) => string;
tolerance?: number; // seconds; default 300; 0 disables timestamp check
}WebhookPresets
Pre-built WebhookVerifyOptions factories for common providers. Call the method and
override secret with your signing secret.
WebhookPresets.stripe(): WebhookVerifyOptions
HMAC-SHA256 using the stripe-signature header. Extracts t and v1 from the
comma-separated header. Tolerance: 300 seconds.
WebhookPresets.github(): WebhookVerifyOptions
HMAC-SHA256 using the x-hub-signature-256 header. Strips the sha256= prefix.
No timestamp tolerance.
WebhookPresets.svix(): WebhookVerifyOptions
Ed25519 using svix-signature and svix-timestamp headers. Extracts the v1, prefixed
signature. Tolerance: 300 seconds.
WebhookMiddleware
Route-level middleware that calls verifyWebhook and returns 401 on failure.
constructor(options: WebhookVerifyOptions)
import { WebhookMiddleware } from '@roostjs/core/webhooks/middleware';
import { WebhookPresets } from '@roostjs/core/webhooks/verify';
const stripeWebhook = new WebhookMiddleware({
...WebhookPresets.stripe(),
secret: env.STRIPE_WEBHOOK_SECRET,
});Pass WebhookMiddleware to a route-specific pipeline or via app.useMiddleware() for a
prefix-matched group.
WebhookVerificationError
class WebhookVerificationError extends Error {
name: 'WebhookVerificationError';
}Thrown by verifyWebhook when verification fails. WebhookMiddleware catches this and
responds with 401 { "error": "<message>" }.
Errors
The following error classes are thrown at runtime but are not exported from @roostjs/core. Catch them by name or use instanceof checks after importing the concrete class from its internal module path (not recommended for production code — prefer guarding with defaults or has() checks instead).
BindingNotFoundError
Thrown by resolve() when no binding is registered for the token.
CircularDependencyError
Thrown when a circular dependency is detected during resolution.
ConfigKeyNotFoundError
Thrown by ConfigManager.get() when the key does not exist and no default is provided.
WebhookVerificationError
Thrown by verifyWebhook() when the signature header is missing, the signature does not
match, or the timestamp is outside the tolerance window.