Core — Application & Router¶
The Application class is the central entry point for Katal. It wires together the router, middleware pipeline, DI container, and every optional subsystem (queue, cache, events, health, …).
Application¶
Constructor¶
import { Application } from "katal";
const app = new Application({
port: 3000, // default: 3000
host: "0.0.0.0", // default: "localhost"
cors: false, // enable built-in CORS pre-flight (basic)
bodyParser: true, // auto-parse JSON / form bodies
});
| Option | Type | Default | Description |
|---|---|---|---|
port |
number |
3000 |
Listening port |
host |
string |
"localhost" |
Binding host |
cors |
boolean |
false |
Enable minimal CORS headers |
bodyParser |
boolean |
true |
Auto-parse JSON request bodies |
Server Lifecycle¶
// Start listening
await app.listen();
// Graceful shutdown
await app.stop();
Get the Router¶
const router = app.getRouter();
router.get("/ping", PingController);
Routing¶
See the full routing reference in HTTP — Controllers. A quick overview:
const router = app.getRouter();
// HTTP verbs
router.get("/users", ListController);
router.post("/users", CreateController);
router.put("/users/:id", ReplaceController);
router.patch("/users/:id", UpdateController);
router.delete("/users/:id", DeleteController);
// Route with named middleware
router.get("/me", MeController, { middleware: ["auth"] });
// Route with inline validation
router.post("/items", CreateItemController, {
validation: { name: { required: true, type: "string" as const } },
});
Route Groups¶
router.group("/api/v1", (r) => {
r.group("/orders", (r) => {
r.get("/", ListOrdersController);
r.post("/", CreateOrderController, { middleware: ["auth"] });
r.get("/:id", GetOrderController);
});
});
Groups are nestable. Middleware defined on a group applies to all nested routes.
Middleware¶
Global Middleware¶
Applied to every request:
import type { Middleware } from "katal";
const logger: Middleware = async (ctx, next) => {
const t = Date.now();
const res = await next(ctx);
console.log(`${ctx.request.method} ${ctx.url.pathname} ${Date.now() - t}ms`);
return res;
};
app.use(logger);
Named Middleware¶
Register once, reference by name on routes:
import { CorsMiddleware, createAuthMiddleware, Auth } from "katal";
const auth = new Auth({ secret: process.env.JWT_SECRET! });
app.middleware("auth", createAuthMiddleware(auth));
app.middleware("cors", new CorsMiddleware({ origins: "*" }));
app.middleware("admin", adminOnlyMiddleware);
Use on a specific route:
router.get("/admin/dashboard", AdminController, {
middleware: ["auth", "admin"],
});
Middleware as Function¶
A Middleware is simply an async function that receives (ctx, next):
type Middleware = (ctx: RequestContext, next: (ctx: RequestContext) => Promise<Response>) => Promise<Response>;
Return a Response directly to short-circuit the pipeline. Call next(ctx) to proceed.
Built-in Middleware¶
| Name | Import | Description |
|---|---|---|
CorsMiddleware |
katal |
CORS headers + pre-flight |
RateLimitMiddleware |
katal |
Sliding-window rate limiter |
createAuthMiddleware |
katal |
JWT authentication |
createOptionalAuthMiddleware |
katal |
JWT auth — user may be null |
createAuthorizationMiddleware |
katal/middleware |
Policy gate (can:<name>) |
| Request observability | app.useRequestObservability() |
Correlation IDs + access log |
Application Methods Reference¶
DI Container¶
app.singleton("db", () => new Database()); // shared instance
app.bind("mailer", () => new Mailer()); // new instance per resolve
app.instance("config", configObject); // register existing value
const db = app.resolve<Database>("db");
Middleware¶
app.use(globalMiddlewareFn);
app.middleware("name", middlewareFn);
Config¶
app.configure<MailConfig>("mail", {
schema: { host: String, port: Number },
defaults: { port: 587 },
});
const mail = app.getConfig<MailConfig>("mail");
Queue¶
app.useInMemoryQueue(); // explicit in-memory queue
app.useQueue(myAdapter); // custom adapter
const queue = app.getQueue(); // lazy-init queue
const driver = app.getQueueDriver(); // "memory" | "custom"
Cache¶
app.useCacheStore(myRedisStore); // plug in a custom CacheStore
const value = await app.cacheGet<string>("key");
await app.cacheSet("key", "value", 60_000); // TTL in ms
await app.cacheDelete("key");
const result = await app.cacheRemember("key", () => compute(), 60_000);
Events¶
app.on("user.created", async (event) => { /* ... */ });
app.once("app.ready", handler);
app.off("user.created", handler);
await app.emit("user.created", { id: 1 });
app.emitSync("metrics.tick", {});
Health Checks¶
app.addHealthCheck("database", async () => {
await db.ping();
return { status: "ok" };
});
app.enableHealthEndpoints();
// exposes GET /health, /health/ready, /health/live
OpenAPI¶
app.exposeOpenApi("/openapi.json");
// Generates from registered routes + validation schemas
Authorization¶
app.definePolicy("can-edit-post", async (ctx) => {
return ctx.user?.id === ctx.params.authorId;
});
// or bulk:
app.definePolicies({
"admin": async (ctx) => ctx.user?.role === "admin",
});
Plugins & Providers¶
app.registerPlugin(myPlugin, { option: "value" });
app.registerProvider(new DatabaseProvider());
app.registerProviders([new CacheProvider(), new MailProvider()]);
Request Observability¶
app.useRequestObservability({
includeBody: false,
logLevel: "info",
});
See Also¶
const mail = app.configure
const resolved = app.getConfig
### Request Handling
```typescript
import { Controller } from 'katal';
class UserController extends Controller {
async handle(request: Request) {
const data = await request.json();
return this.json({ success: true, data });
}
}
app.getRouter().post('/users', UserController);
Configuration¶
The Application accepts the following configuration options:
interface AppConfig {
port: number; // Default: 3000
host: string; // Default: "localhost"
cors: boolean; // Default: false
bodyParser: boolean; // Default: true
}
Request Context¶
Each route handler receives a context object:
interface RequestContext {
request: Request;
id: string; // Request ID
state: Record<string, unknown>; // Shared per-request state
params: Record<string, string>; // URL parameters
query: Record<string, string>; // Query string parameters
body: any; // Parsed request body
user?: User; // Authenticated user (if available)
}
Error Handling¶
The framework includes built-in RFC-style Problem Details responses (application/problem+json) for framework-level errors such as 404, 422, and 500:
{
"type": "https://httpstatuses.com/500",
"title": "Internal Server Error",
"status": 500,
"detail": "...",
"instance": "http://localhost:3000/users",
"traceId": "..."
}
Server Lifecycle¶
// Boot without starting server
await app.boot();
// Start server
await app.listen(3000, 'localhost');
// Stop server
await app.stop();