Skip to content

@mantlejs/http

Zero-dependency HTTP transport adapter for Mantle JS. Exposes two handlers from a single http() plugin: a Node.js (req, res) handler for use with http.createServer, and a Fetch API (request: Request) => Promise<Response> handler for Cloudflare Workers, Vercel Edge Functions, AWS Lambda@Edge, and other edge runtimes. No Express or Koa required — route matching and body parsing are implemented from scratch using only Node.js built-ins.


Terminal window
npm install @mantlejs/http

http() registers two handlers on the Mantle application:

  • httpHandler (NodeHttpHandler) — a (req: IncomingMessage, res: ServerResponse) => void callback compatible with Node.js http.createServer. Calling app.listen(port) automatically creates a server using this handler.
  • fetchHandler (FetchHandler) — a (request: Request) => Promise<Response> callback for edge runtimes that accept standard Fetch API objects.

Both handlers run the full Mantle hook pipeline and set params.provider = 'http'.

@mantlejs/http only depends on @mantlejs/mantle and Node.js built-ins. Route matching and JSON body parsing are implemented from scratch — no Express, Koa, or other HTTP framework is pulled in.

fetchHandler is compatible with any runtime that supports the WinterCG minimum common API — Request, Response, URL, and crypto.randomUUID(). Tested platforms:

Platform Runtime Import style
Cloudflare Workers V8 isolate npm (bundled)
Vercel Edge Functions V8 isolate npm (bundled)
Supabase Edge Functions Deno npm: specifier
Deno Deploy Deno npm: specifier
AWS Lambda@Edge Node.js npm
Bun Bun npm

Edge runtimes are stateless and ephemeral — each isolate handles one request (or a small batch). This means app.listen() is irrelevant in these environments; retrieve the fetchHandler directly instead. Anything requiring a persistent connection (database pool, socket.io, Redis) must be re-established per request or routed through a connection proxy.

Every request gets an x-correlation-id header in the response. If the incoming request supplies one, it is echoed back. The correlation ID is available inside hooks via getContext().correlationId.


import { mantle } from "@mantlejs/mantle";
import { http } from "@mantlejs/http";
const app = mantle().configure(http());
app.use("users", new UserService(new UserRepository(app)));
app.listen(3030, () => console.log("Listening on :3030"));
import { mantle } from "@mantlejs/mantle";
import { http } from "@mantlejs/http";
import type { FetchHandler } from "@mantlejs/http";
const app = mantle().configure(http());
app.use("users", new UserService(new UserRepository(app)));
const handler = app.get("fetchHandler") as FetchHandler;
export default { fetch: handler };
app/api/users/route.ts
import { mantle } from "@mantlejs/mantle";
import { http } from "@mantlejs/http";
import type { FetchHandler } from "@mantlejs/http";
export const runtime = "edge";
const app = mantle().configure(http());
app.use("users", new UserService(new UserRepository(app)));
const handler = app.get("fetchHandler") as FetchHandler;
export const GET = handler;
export const POST = handler;
export const PUT = handler;
export const PATCH = handler;
export const DELETE = handler;

Supabase Edge Functions run on Deno. Import the packages using the npm: specifier:

supabase/functions/api/index.ts
import { mantle } from "npm:@mantlejs/mantle";
import { http } from "npm:@mantlejs/http";
import type { FetchHandler } from "npm:@mantlejs/http";
const app = mantle().configure(http());
app.use("users", new UserService(new UserRepository(app)));
const handler = app.get("fetchHandler") as FetchHandler;
Deno.serve(handler);
import { createServer } from "node:http";
import { mantle } from "@mantlejs/mantle";
import { http } from "@mantlejs/http";
import type { NodeHttpHandler } from "@mantlejs/http";
const app = mantle().configure(http());
app.use("users", new UserService(new UserRepository(app)));
const handler = app.get("httpHandler") as NodeHttpHandler;
createServer(handler).listen(3030);

Returns a MantlePlugin that registers the HTTP transport on the application.

Field Type Default Description
introspection boolean | { path?: string } false Mount the introspection endpoint; pass { path } to customize it.
batch boolean | { path?: string; maxSize?: number } true Mount the POST /batch endpoint; false disables it, { path, maxSize } configures it.
cors boolean | CorsOptions false Enable CORS via hand-rolled header logic; true uses permissive defaults, an object customizes origin/methods/headers/credentials.

With introspection enabled, GET /_services (or the custom path) returns a ServiceDescriptor[] — one entry per registered service with path, methods, events, schema, the repository’s capabilities (when the service exposes them, e.g. RepositoryService), and authRequired. Off by default; without the option the route 404s.

app.configure(http({ introspection: true }));

After calling app.configure(http()):

app.get(key) Type Description
'httpHandler' NodeHttpHandler Node.js (req, res) => void handler for http.createServer
'fetchHandler' FetchHandler Fetch API (request) => Promise<Response> handler for edge runtimes
'http:router' HttpRouterLike The transport-neutral contract other Mantle packages (@mantlejs/auth-oauth, @mantlejs/mcp, @mantlejs/openapi) use to mount their own raw routes regardless of which HTTP transport is configured
'http:server' http.Server Set after app.listen() is called
HTTP method Path Mantle method
GET /service find
GET /service/:id get
POST /service create
PUT /service/:id update
PATCH /service/:id patch
DELETE /service/:id remove
POST /service/:method custom method

POST /batch (mounted by default, on both handlers) dispatches an array of calls through app.batch() in one round trip:

Terminal window
curl -X POST http://localhost:3030/batch -H "content-type: application/json" -d '[
{ "service": "users", "method": "get", "id": 1 },
{ "service": "messages", "method": "find", "params": { "query": { "$limit": 5 } } }
]'

Each call runs the target service’s full hook pipeline (including authenticate("jwt")) with the batch request’s headers — batch is not a way to bypass authentication or validation. The response is a BatchResult[] in the same order as the request array; calls execute concurrently and each entry independently reports { status: "success", result } or { status: "error", error } (no cross-call atomicity). Batches over maxSize (default 25) are rejected with 400 BadRequest before any call executes.

app.configure(http({ batch: false })); // disable
app.configure(http({ batch: { path: "/_batch", maxSize: 50 } }));

@mantlejs/client can coalesce same-tick service calls into this endpoint automatically — see its batch option.

Disabled by default — consistent with Mantle’s secure-by-default posture elsewhere. Enable it with cors: true for permissive defaults, or pass a CorsOptions object to customize:

app.configure(http({ cors: true })); // reflects Origin, allows GET/POST/PUT/PATCH/DELETE, no credentials
app.configure(
http({
cors: { origin: ["https://app.example.com"], credentials: true },
}),
);

CorsOptions (exported from @mantlejs/mantle) is shared across @mantlejs/express, @mantlejs/koa, and @mantlejs/http so switching transports doesn’t require relearning CORS configuration:

Field Type Default Description
origin boolean | string | string[] | ((origin) => boolean | string) true (reflect the request’s Origin) Allowed origin(s) for Access-Control-Allow-Origin.
methods string[] ["GET", "POST", "PUT", "PATCH", "DELETE"] Allowed methods for Access-Control-Allow-Methods.
allowedHeaders string[] reflects Access-Control-Request-Headers Allowed request headers.
exposedHeaders string[] none Headers exposed to the browser.
credentials boolean false Allow credentials (cookies, Authorization) cross-origin.
maxAge number none Preflight cache duration in seconds.

@mantlejs/http has no framework to delegate to, so CORS is hand-rolled: headers are set on every response for an allowed origin, and an OPTIONS preflight short-circuits with a 204 (on both httpHandler and fetchHandler) before the request reaches the router.

Utility exported for use in other adapters or middleware. Maps a MantleError (or any error) to { status: number; body: Record<string, unknown> }.


import type { NodeHttpHandler, FetchHandler, HttpOptions } from "@mantlejs/http";
// Node.js handler
type NodeHttpHandler = (req: IncomingMessage, res: ServerResponse) => void;
// Edge / Fetch API handler
type FetchHandler = (request: Request) => Promise<Response>;
// Options accepted by http()
interface HttpOptions {
introspection?: boolean | { path?: string };
batch?: boolean | { path?: string; maxSize?: number };
cors?: boolean | CorsOptions;
}

Terminal window
npx nx build http # compile
npx nx test http # run tests
npx nx lint http # lint

Build before publishing:

Terminal window
npx nx build http

First publish (scoped packages require --access public):

Terminal window
cd packages/http
npm publish --access public

Subsequent releases — bump version in packages/http/package.json, then:

Terminal window
cd packages/http
npm publish