Dhal v1.1.1 ships dedicated adapters for Express, Fastify, NestJS, Koa, Hono on Node.js, universal Web Request/Response handlers, TanStack Start on Node.js, and raw node:http. Every adapter reuses the same Dhal engine, configuration model, stores, telemetry, and decision lifecycle.
Express
import express from "express";
import { dhal } from "@rokadhq/dhal/express";
const app = express();
app.use(express.json({ limit: "1mb" }));
app.use(dhal({ configPath: "dhal.json" }));Place body parsers before Dhal when body-aware rules should inspect parsed payloads. The adapter reads req.body, an optional req.rawBody, identity fields, and the final response status.
Use dhalFromEngine(engine) when you create the engine yourself for custom stores, telemetry, or lifecycle control.
Fastify
import Fastify from "fastify";
import { dhalFastify } from "@rokadhq/dhal/fastify";
const app = Fastify();
await app.register(dhalFastify({ configPath: "dhal.json" }));Dhal inspects requests in preHandler and records response outcomes in onResponse. Use dhalFastifyFromEngine(engine) with a pre-created engine.
NestJS
import { NestFactory } from "@nestjs/core";
import { installDhalNest } from "@rokadhq/dhal/nest";
import { AppModule } from "./app.module.js";
const app = await NestFactory.create(AppModule);
const dhal = await installDhalNest(app, { configPath: "dhal.json" });
await app.listen(3000);Install Dhal after creating the Nest application and before app.listen(). Dhal detects whether the Nest HTTP application uses Express or Fastify and installs the corresponding stable adapter.
For an existing engine:
import { installDhalNestFromEngine } from "@rokadhq/dhal/nest";
const installation = await installDhalNestFromEngine(app, engine);The returned installation exposes engine, the detected platform, and close(). Nest microservice transports without an HTTP adapter are outside this integration.
Koa
import Koa from "koa";
import { dhalKoa } from "@rokadhq/dhal/koa";
const app = new Koa();
app.use(dhalKoa({ configPath: "dhal.json" }));Register Dhal before application routes. The adapter inspects before downstream middleware, terminates the chain for blocked requests, sets x-dhal-action and x-dhal-rule, records the downstream response status, and reads optional identities from context.state.
Use dhalKoaFromEngine(engine) for custom lifecycle management.
Hono on Node.js
import { Hono } from "hono";
import { dhalHono } from "@rokadhq/dhal/hono";
const app = new Hono();
app.use("*", dhalHono({ configPath: "dhal.json" }));The Hono adapter consumes standard Web Request and Response objects, returns a controlled response when Dhal blocks, and records the final downstream response status. Identity values can be exposed through context.var.userId, tenantId, apiKeyId, or context.var.user.id.
The supported target is Hono running on Node.js. Edge runtimes are not included in the compatibility commitment. Use dhalHonoFromEngine(engine) for an existing engine.
Universal Fetch handlers
import { withDhalFetch } from "@rokadhq/dhal/fetch";
export const handler = withDhalFetch(async (request) => {
return Response.json({ path: new URL(request.url).pathname });
}, {
configPath: "dhal.json",
route: "/api/*"
});Use @rokadhq/dhal/fetch for Node.js frameworks that expose Web Request and Response handlers. It supports identity callbacks, explicit route metadata, controlled blocking responses, response-outcome recording, and bounded opt-in body inspection:
withDhalFetch(handler, {
configPath: "dhal.json",
body: {
enabled: true,
maxBytes: 65536,
parseJson: true,
parseFormUrlEncoded: true
}
});The adapter reads a cloned body only when enabled and leaves the original request available to the application handler.
TanStack Start on Node.js
import handler, { createServerEntry } from "@tanstack/react-start/server-entry";
import { withDhalStart } from "@rokadhq/dhal/tanstack-start";
export default createServerEntry({
fetch: withDhalStart(handler.fetch, {
configPath: "dhal.json"
})
});TanStack Start's server entry uses a universal Fetch handler and handles SSR, server routes, and server-function requests. Dhal wraps that handler so these requests pass through the same route-aware protection model.
Request context can provide trusted identity values:
withDhalStart(handler.fetch, {
configPath: "dhal.json",
identity: (_request, options) => ({
userId: options?.context?.user?.id,
tenantId: options?.context?.tenant?.id
})
});Raw node:http
import http from "node:http";
import { createNodeHttpDhal } from "@rokadhq/dhal/node-http";
const protection = createNodeHttpDhal({ configPath: "dhal.json" });
const server = http.createServer(async (req, res) => {
const decision = await protection.inspect(req, res);
if (decision.action === "block") return;
res.statusCode = 200;
res.end("ok");
});The raw adapter normalizes method, URL, path, headers, client IP, identity headers, and content-length. It does not consume the request stream.
Custom integrations with the core engine
import { createDhal, type DhalRequest } from "@rokadhq/dhal";
const protection = createDhal({ configPath: "dhal.json" });
const request: DhalRequest = {
method: "POST",
url: "/api/orders?source=web",
path: "/api/orders",
route: "/api/orders",
headers: { "content-type": "application/json" },
ip: "203.0.113.10",
body: { productId: "sku_123", quantity: 1 },
userId: "user_123",
tenantId: "tenant_456"
};
const decision = await protection.inspect(request);After the application response is known, call recordOutcome() so response-aware signals can be updated:
await protection.recordOutcome(request, { statusCode: 401 });Lifecycle and blocking responses
Create the engine yourself when you need explicit shutdown control:
process.once("SIGTERM", async () => {
await protection.close(5_000);
process.exit(0);
});Bundled adapters set x-dhal-action: block and x-dhal-rule: <rule id>, then return the configured status code and controlled JSON response. Do not expose private decision metadata to untrusted clients.