@roostjs/core Guides
Task-oriented instructions for the container, config, middleware, and service providers.
How to register a service provider
Service providers are the correct place to register container bindings and run boot logic.
import { ServiceProvider } from '@roostjs/core';
export class CacheServiceProvider extends ServiceProvider {
async register(): Promise<void> {
this.app.container.singleton(CacheService, (c) => {
return new CacheService(c.resolve(ConfigManager));
});
}
async boot(): Promise<void> {
const cache = this.app.container.resolve(CacheService);
await cache.warmUp();
}
}import { Application } from '@roostjs/core';
import { CacheServiceProvider } from './providers/CacheServiceProvider';
const app = Application.create(env, config);
app.register(CacheServiceProvider);
await app.boot();The register() method runs before all boot() calls, so you can safely resolve services registered by other providers inside boot(). See @roostjs/core reference for full ServiceProvider API.
How to configure dependency injection bindings
Use singleton for services that should be shared across the request, and bind for services that need a fresh instance each time.
import { RoostContainer } from '@roostjs/core';
const container = new RoostContainer();
// One instance per container lifetime
container.singleton(Database, (c) => {
return new Database(c.resolve(ConfigManager));
});
// New instance on every resolve
container.bind(RequestLogger, (c) => {
return new RequestLogger(c.resolve(ConfigManager));
});
// Resolve anywhere
const db = container.resolve(Database);For request-level isolation, create a scoped child container. Scoped containers inherit parent bindings but maintain their own singleton instances.
const requestContainer = appContainer.scoped();
// Singletons in requestContainer are scoped to this requestHow to create custom middleware
Middleware is a function or class that wraps the request/response cycle. Return early to short-circuit.
import type { Handler } from '@roostjs/core';
export async function rateLimitMiddleware(request: Request, next: Handler): Promise<Response> {
const ip = request.headers.get('cf-connecting-ip') ?? 'unknown';
const allowed = await checkRateLimit(ip);
if (!allowed) {
return new Response('Too Many Requests', { status: 429 });
}
return next(request);
}
// With parameters (curried)
export function rateLimit(max: number) {
return async function (request: Request, next: Handler): Promise<Response> {
const allowed = await checkRateLimit(request, max);
if (!allowed) return new Response('Too Many Requests', { status: 429 });
return next(request);
};
}app.useMiddleware(rateLimitMiddleware);
// Or with parameters:
app.useMiddleware(rateLimit(100));For class-based middleware, extend the framework's middleware class and use withContainer to get DI access. See Pipeline reference for the class middleware interface.
How to access configuration values
Use ConfigManager with dot-notation keys. Pass a default to avoid throwing on missing keys.
import { ConfigManager } from '@roostjs/core';
const config = new ConfigManager({
app: { name: 'My App', debug: false },
database: { default: 'd1' },
});
// Throws ConfigKeyNotFoundError if missing and no default
const name = config.get('app.name');
// Safe: returns 'UTC' if key is absent
const timezone = config.get('app.timezone', 'UTC');
// Set values at runtime
config.set('features.darkMode', true);
// Check existence
if (config.has('stripe.secretKey')) {
// ...
}In a service provider, resolve ConfigManager from the container rather than constructing it directly — the application registers it automatically.
How to build a middleware pipeline
Use Pipeline to compose multiple middleware functions in order. Each middleware calls next to pass control forward.
import { Pipeline } from '@roostjs/core';
const pipeline = new Pipeline()
.use(loggerMiddleware)
.use(rateLimitMiddleware)
.use(authMiddleware);
const response = await pipeline.handle(request, async (req) => {
// Final handler — req has passed all middleware
return new Response(JSON.stringify({ ok: true }), {
headers: { 'content-type': 'application/json' },
});
});To inject container dependencies into class-based middleware, chain .withContainer(container) before calling handle.
const pipeline = new Pipeline()
.withContainer(container)
.use(AuthMiddlewareClass);How to add structured logging with request tracing
Add RequestIdMiddleware as the first global middleware. It generates a trace ID from
the cf-ray header (or crypto.randomUUID()) and binds a Logger into the
request-scoped container so every handler can resolve it.
import { Application } from '@roostjs/core';
import { RequestIdMiddleware } from '@roostjs/core/middleware/request-id';
const app = Application.create(env);
app.useMiddleware(RequestIdMiddleware);Resolve the logger inside a handler or downstream middleware:
import { Logger } from '@roostjs/core';
export async function getUser(request: Request): Promise<Response> {
const container = (request as any).__roostContainer;
const logger = container.resolve(Logger);
logger.info('user.fetch', { userId: request.params?.id });
const user = await User.find(request.params.id);
if (!user) {
logger.warn('user.not_found', { userId: request.params?.id });
return new Response('Not Found', { status: 404 });
}
return Response.json(user);
}Every log line is emitted as a single-line JSON object including requestId, method,
path, level, message, and timestamp. The requestId is also set on the response
as X-Request-Id.
How to test logging
Use Logger.fake() to get a FakeLogger that captures entries in memory instead of
writing to console.log.
import { Logger } from '@roostjs/core';
test('logs a warning when user is not found', async () => {
const logger = Logger.fake();
await getUser(fakeRequest({ params: { id: 'missing' } }), logger);
logger.assertLogged('warn', 'user.not_found');
logger.assertNotLogged('error');
logger.restore();
});Logger.fake() accepts an optional Partial<LogContext> to override default values
(requestId: 'fake-request-id', method: 'GET', path: '/').
How to run background work after a response
Use app.defer() to register a promise that should complete after the response is sent.
The Worker runtime will not terminate the isolate until all deferred promises settle.
import { Application } from '@roostjs/core';
const app = Application.create(env);
app.onDispatch(async (request) => {
const result = await handleRequest(request);
// This write completes after the response is returned to the client
app.defer(analytics.track({ event: 'request.completed', path: new URL(request.url).pathname }));
return Response.json(result);
});Pass the Cloudflare ExecutionContext to app.handle() so defer() has a context to
call waitUntil() on:
export default {
fetch(request: Request, env: Env, ctx: ExecutionContext) {
return app.handle(request, ctx);
},
};defer() silently does nothing when no context is available, so tests that call
app.handle(request) without a second argument work without modification.
How to verify webhook signatures
Using a preset
Most providers are covered by WebhookPresets. Override the secret field with your
signing key and pass the options to WebhookMiddleware.
import { WebhookMiddleware } from '@roostjs/core/webhooks/middleware';
import { WebhookPresets } from '@roostjs/core/webhooks/verify';
const middleware = new WebhookMiddleware({
...WebhookPresets.stripe(),
secret: env.STRIPE_WEBHOOK_SECRET,
});
// Wire into a route-level pipeline or app.useMiddleware() for a prefix groupAvailable presets: WebhookPresets.stripe(), WebhookPresets.github(),
WebhookPresets.svix().
Calling verifyWebhook directly
For providers not covered by a preset, call verifyWebhook directly and supply custom
parsing logic.
import { verifyWebhook, WebhookVerificationError } from '@roostjs/core/webhooks/verify';
export async function handleCustomWebhook(request: Request): Promise<Response> {
let body: string;
try {
body = await verifyWebhook(request, {
secret: env.CUSTOM_WEBHOOK_SECRET,
headerName: 'x-custom-signature',
algorithm: 'hmac-sha256',
buildSignedPayload: (_ts, b) => b,
tolerance: 0,
});
} catch (err) {
if (err instanceof WebhookVerificationError) {
return new Response('Unauthorized', { status: 401 });
}
throw err;
}
const payload = JSON.parse(body);
// ...
return new Response(null, { status: 204 });
}