Programmatic API Reference
This document provides a comprehensive reference for the Setu-TS programmatic API. For decorator-based usage, see Decorators Guide.
Application
createApplication(options?)
Creates a new application instance.
import type { MiddlewareFunction } from '@setu-ts/common';
import { createApplication } from '@setu-ts/kernel';
const app = createApplication();
Options: Plugins can be passed inline at creation time (equivalent to calling register()
immediately after):
import { createApplication } from '@setu-ts/kernel';
import { RuntimePlugin } from '@setu-ts/runtime';
const app = createApplication({
plugins: [RuntimePlugin()],
});
Or registered individually:
const app = createApplication();
app.register(RuntimePlugin());
Application Methods
register(plugin, options?)
Register a plugin with the application.
import { RuntimePlugin } from '@setu-ts/runtime';
import { LoggerPlugin } from '@setu-ts/logger-plugin';
app.register(RuntimePlugin());
app.register(LoggerPlugin({ level: 'info' }));
router.get(path, handler)
Register a GET route handler through the router.
import { CAPABILITIES } from '@setu-ts/common';
import type { IDatabaseService } from '@setu-ts/database-plugin';
app.router.get('/users', async (ctx) => {
const usersRepo = ctx.services
.get<IDatabaseService>(CAPABILITIES.DATABASE)
.getRepository('users');
const users = await usersRepo.findAll();
return ctx.response.json(users);
});
router.post(path, handler), router.put(path, handler), router.patch(path, handler), router.delete(path, handler)
Register route handlers for other HTTP methods.
const db = ctx.services.get<IDatabaseService>(CAPABILITIES.DATABASE);
const usersRepo = db.getRepository<{ id: string; name: string }>('users');
app.router.post('/users', async (ctx) => {
const user = await usersRepo.create({ name: 'alice' });
return ctx.response.json(user);
});
middleware.add(middleware)
Add global middleware to the pipeline.
import { MetricsPlugin } from '@setu-ts/metrics-plugin';
app.register(MetricsPlugin({ endpoint: '/metrics' }));
start(options?)
Start the application and optionally listen on a port.
await app.start({ port: 3000, hostname: '0.0.0.0' });
Options:
port?: number- Port to listen onhostname?: string- Hostname to bind to (default: ‘0.0.0.0’)
stop()
Stop the application gracefully.
await app.stop();
fetch(request)
Handle a web-standard Request. Used for testing and Workers deployments.
const response = await app.fetch(new Request('http://localhost:3000/health'));
inject(request)
Inject a request without a network socket (testing only).
import { createApplication } from '@setu-ts/kernel';
import { inject } from '@setu-ts/testing';
import { RuntimePlugin } from '@setu-ts/runtime';
const app = createApplication({ plugins: [RuntimePlugin()] });
app.router.get('/health', (ctx) => ctx.response.json({ status: 'up' }));
await app.start();
const response = await inject(app, {
method: 'GET',
url: '/health',
});
if (response.statusCode !== 200 || response.json<{ status: string }>().status !== 'up') {
throw new Error('health injection failed');
}
await app.stop();
Router
Route Handler Context
Route handlers receive a context object with the following properties:
// The actual IRequestContext interface (from @setu-ts/common) — see the real
// declaration in packages/common/src/http.ts for the authoritative contract.
// Key members:
// request: IRequest (see IRequest below)
// response: IResponse (fluent terminal methods, no ResponseOptions bag)
// services: IServiceRegistry (typed capability resolution)
// params: Readonly<Record<string, string>>
// state: Map<string, unknown>
// signal: AbortSignal (aborts when the client disconnects)
Response Methods
// JSON response
ctx.response.json(data);
// Text response
ctx.response.text('Hello');
// Redirect
ctx.response.redirect('/other');
// Streaming response
ctx.response.stream(readableStream);
Response Methods (fluent API)
IResponse methods return HandlerResult (terminal) or IResponse (fluent). There is no separate
ResponseOptions bag — each method carries its own parameters:
// Fluent setters (chainable)
ctx.response.status(200);
ctx.response.header('X-Custom', 'value');
// Terminal methods (return HandlerResult)
ctx.response.json({ key: 'value' });
ctx.response.text('Hello');
ctx.response.redirect('/other', 302);
ctx.response.stream(readableStream);
// Read the final snapshot after the handler completes
const snapshot = ctx.response.snapshot();
Service Registry
register<T>(token, service, options?)
Register a service.
ctx.services.register<IMyService>('my-service', new MyService());
get<T>(token)
Resolve a service.
const db = ctx.services.get<IDatabaseService>(CAPABILITIES.DATABASE);
has(token)
Check if a service is registered.
if (ctx.services.has(CAPABILITIES.CACHE)) {
const cache = ctx.services.get<ICacheStore>(CAPABILITIES.CACHE);
}
getAll<T>(token)
Get all providers for a multi-provider capability.
const validators = ctx.services.getAll<IValidator>('validator');
Middleware
Creating Middleware
const myMiddleware: MiddlewareFunction = async (ctx, next) => {
console.log('Before');
await next();
console.log('After');
};
Middleware Priority
app.middleware.add(myMiddleware, { priority: 25 });
Default priority is 500. Lower numbers run first.
Lifecycle Hooks
onRegister(handler)
Called during plugin registration.
ctx.lifecycle.onRegister(() => {
console.log('Registering...');
});
onInit(handler)
Called after all plugins have registered.
ctx.lifecycle.onInit(() => {
console.log('Initializing...');
});
onBootstrap(handler)
Called when the application is ready to accept requests.
ctx.lifecycle.onBootstrap(() => {
console.log('Ready!');
});
onRequest(handler), onResponse(handler), onError(handler)
Per-request lifecycle hooks.
ctx.lifecycle.onRequest((ctx) => {
console.log('Request started:', ctx.request.url);
});
ctx.lifecycle.onResponse((ctx) => {
console.log('Response sent:', ctx.response.snapshot().status);
});
ctx.lifecycle.onError((error, ctx) => {
console.error('Error:', error);
});
onStopping(handler), onShutdown(handler), onClose(handler)
Shutdown lifecycle hooks. The kernel owns the guard, drain, and socket close. The actual shutdown order is: stopping hooks while requests are still accepted → refuse new requests → drain in-flight requests → close the server socket → shutdown hooks → close hooks. A rejecting stopping hook is reported only after the remaining shutdown phases finish.
ctx.lifecycle.onStopping(() => {
// Tell an external load balancer or service registry to stop sending traffic.
});
ctx.lifecycle.onShutdown(() => {
// Flush buffers and close resources after the kernel drains and closes the socket.
});
ctx.lifecycle.onClose(() => {
// Release resources
});
Request/Response
IRequest (exact contract from @setu-ts/common)
Transcribed exactly from the IRequest interface in @setu-ts/common; mutable middleware-owned
fields deliberately remain mutable.
interface IRequest {
readonly method: HttpMethod; // 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS'
readonly url: string;
readonly path: string;
readonly headers: Headers;
readonly ip?: string;
user?: IPrincipal; // populated by auth middleware
tenant?: ITenant; // populated by multi-tenancy middleware
signal?: AbortSignal; // fires on client disconnect
readonly raw?: Request; // The undisturbed web-standard Request, preserved for WebSocket upgrade and gRPC dispatch after the middleware pipeline.
// Body readers (consume the body exactly once)
json<T = unknown>(): Promise<T>;
text(): Promise<string>;
bytes(): Promise<Uint8Array>;
formData?(): Promise<FormBody>; // optional (M94b): both form encodings, shared parseFormBody
}
Note: IRequest has no query field (query parsing happens in the router), no body field
(body is read through the dedicated methods above), and no bodyUsed property. formData is
OPTIONAL on the signal/raw precedent. Nothing falls back automatically: the framework’s own form
consumers (the upload middleware and the CSRF verifier) call
parseFormBody(await request.bytes(), request.headers.get('content-type')) from @setu-ts/common
themselves when a request omits the accessor, and application code reading a form from a custom
IRequest does the same. Either route throws the 415-branded UnsupportedFormEncodingError for a
non-form content-type; what the accessor adds over the fallback is memoization, so two middleware
reading one body trigger a single parse.
IRequestContext
interface IRequestContext {
readonly id: string;
readonly request: IRequest;
readonly response: IResponse;
readonly services: IServiceRegistry;
readonly params: Readonly<Record<string, string>>;
readonly query: Readonly<Record<string, string>>;
readonly state: Map<string, unknown>;
readonly startTime: number;
readonly signal: AbortSignal;
}
IResponse
interface IResponse {
status(code: number): IResponse;
header(name: string, value: string): IResponse;
appendHeader(name: string, value: string): IResponse;
json<T>(body: T): HandlerResult;
text(body: string): HandlerResult;
send(body?: Uint8Array): HandlerResult;
redirect(url: string, status?: number): HandlerResult;
stream(body: ReadableStream<Uint8Array>): HandlerResult;
snapshot(): ResponseSnapshot;
}
Health Checks
register(name, check)
Register a health check.
import { CAPABILITIES } from '@setu-ts/common';
import type { IRuntimeServices } from '@setu-ts/common';
const runtime = ctx.services.get<IRuntimeServices>(CAPABILITIES.RUNTIME);
ctx.health.register('database', async () => {
return { status: 'up', data: { timestamp: runtime.now() } };
});
Metrics
Obtaining the Metrics Service
Resolve IMetricsService through the capability token:
import { CAPABILITIES } from '@setu-ts/common';
import type { ICounter, IMetricsService } from '@setu-ts/common';
const metrics = ctx.services.get<IMetricsService>(CAPABILITIES.METRICS);
Creating a Custom Counter
Use counter() to get or create an ICounter, then call inc() with labels:
const counter = metrics.counter('my_requests_total', {
help: 'Total requests handled by my service',
labels: ['method', 'path'],
});
// Increment the counter explicitly in your middleware or handler
counter.inc(1, { method: 'GET', path: '/users' });
Note: A custom contributed counter is NOT automatically observed by any built-in HTTP collector.
The built-in HTTP collectors track their own metrics (http_requests_total,
http_request_duration_seconds, etc.). To record a custom counter you must call inc() (or
observe()) explicitly.
OpenAPI Contributions
addSchema(name, schema)
Contribute an OpenAPI schema.
ctx.openapi.addSchema('User', {
type: 'object',
properties: {
id: { type: 'string' },
name: { type: 'string' },
},
});
CLI Contributions
register(name, handler)
Register a CLI command.
ctx.cli.register('my-command', async (args) => {
console.log('Command executed with args:', args);
});
Decorator Contributions
register(name, handler)
Register a decorator handler.
ctx.decorators.register('MyDecorator', async (metadata, target) => {
// Handle decorator application
});
Runtime Services
IRuntimeServices
type RuntimeSignal = 'SIGTERM' | 'SIGINT';
interface IRuntimeServices {
platform(): RuntimePlatform;
version(): string;
hostname(): string;
uuid(): string;
randomBytes(length: number): Uint8Array;
readonly subtle: SubtleCrypto;
now(): number;
hrtime(): number;
setTimeout(fn: () => void, ms: number): TimerHandle;
clearTimeout(handle: TimerHandle): void;
setInterval(fn: () => void, ms: number): TimerHandle;
clearInterval(handle: TimerHandle): void;
readonly env: Readonly<Record<string, string | undefined>>;
exit(code?: number): never;
readonly fs?: IFileSystem;
readonly workers?: IWorkerHost;
readonly dns?: IDnsResolver;
onSignal?(signal: RuntimeSignal, handler: () => void): void;
}
onSignal registers a graceful-shutdown handler, so application code never needs a runtime’s own
signal API. It is optional in two directions, and both matter: Cloudflare Workers omits it
because an isolate is evicted rather than signalled, and the Deno adapter omits it on Windows,
where registering for SIGTERM throws. Call it as runtime.onSignal?.(...) — a present key means
“this runtime can register one”, never “this platform raises signals”.
Runtime Platform
type RuntimePlatform = 'deno' | 'node' | 'bun' | 'cloudflare-workers';
Every value has a runtime implementation — there is no 'unknown' arm. Use
detectRuntime() to resolve the current
platform, or pass RuntimePlugin({ platform }) to force one.
Testing Utilities
createTestApp(options?)
Create a test application.
import { createTestApp } from '@setu-ts/testing';
const app = await createTestApp({
plugins: [RuntimePlugin()],
});
inject(app, request)
Inject a request.
import { createApplication } from '@setu-ts/kernel';
import { inject } from '@setu-ts/testing';
import { RuntimePlugin } from '@setu-ts/runtime';
const app = createApplication({ plugins: [RuntimePlugin()] });
app.router.post('/test', async (ctx) => {
return ctx.response.status(201).json(await ctx.request.json<{ foo: string }>());
});
await app.start();
const response = await inject(app, {
method: 'POST',
url: '/test',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ foo: 'bar' }),
});
if (response.statusCode !== 201 || response.json<{ foo: string }>().foo !== 'bar') {
throw new Error('request injection failed');
}
await app.stop();
createMockPlugin(options?)
Create a mock plugin.
import { createMockPlugin } from '@setu-ts/testing';
const mockPlugin = createMockPlugin({
name: 'my-service',
service: mockMyService,
});
Streaming
Streaming Responses
import { CAPABILITIES } from '@setu-ts/common';
import type { IRuntimeServices } from '@setu-ts/common';
const runtime = app.services.get<IRuntimeServices>(CAPABILITIES.RUNTIME);
const delay = (ms: number) =>
new Promise<void>((resolve) => {
runtime.setTimeout(resolve, ms);
});
app.router.get('/stream', async (ctx) => {
const stream = new ReadableStream({
async start(controller) {
for (let i = 0; i < 10; i++) {
controller.enqueue(new TextEncoder().encode(`Line ${i}\n`));
await delay(100);
}
controller.close();
},
});
return ctx.response.stream(stream);
});
Client Disconnect Handling
import { CAPABILITIES } from '@setu-ts/common';
import type { IRuntimeServices } from '@setu-ts/common';
const runtime = app.services.get<IRuntimeServices>(CAPABILITIES.RUNTIME);
const delay = (ms: number) =>
new Promise<void>((resolve) => {
runtime.setTimeout(resolve, ms);
});
app.router.get('/long-running', async (ctx) => {
const stream = new ReadableStream({
async start(controller) {
try {
while (!ctx.signal.aborted) {
controller.enqueue(new TextEncoder().encode('data\n'));
await delay(1000);
}
} catch {
// Client disconnected
} finally {
controller.close();
}
},
});
return ctx.response.stream(stream);
});
Next Steps
- Decorators Guide - Optional decorator API
- Plugin Architecture - Deep dive into plugins
- Examples - Runnable examples