@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.
Installation
Section titled “Installation”npm install @mantlejs/httpConcepts
Section titled “Concepts”Two handlers, one plugin
Section titled “Two handlers, one plugin”http() registers two handlers on the Mantle application:
httpHandler(NodeHttpHandler) — a(req: IncomingMessage, res: ServerResponse) => voidcallback compatible with Node.jshttp.createServer. Callingapp.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'.
Zero dependencies
Section titled “Zero dependencies”@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.
Edge runtime compatibility
Section titled “Edge runtime compatibility”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.
Correlation IDs
Section titled “Correlation IDs”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.
Quick start
Section titled “Quick start”Node.js server
Section titled “Node.js server”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"));Cloudflare Workers
Section titled “Cloudflare Workers”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 };Vercel Edge Functions
Section titled “Vercel Edge Functions”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
Section titled “Supabase Edge Functions”Supabase Edge Functions run on Deno. Import the packages using the npm: specifier:
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);Custom Node.js server
Section titled “Custom Node.js server”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);http(options?)
Section titled “http(options?)”Returns a MantlePlugin that registers the HTTP transport on the application.
HttpOptions
Section titled “HttpOptions”| 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 |
Route conventions
Section titled “Route conventions”| 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 |
Batch endpoint
Section titled “Batch endpoint”POST /batch (mounted by default, on both handlers) dispatches an array of calls through
app.batch() in one round trip:
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 })); // disableapp.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 credentialsapp.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.
toErrorResponse(err)
Section titled “toErrorResponse(err)”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 handlertype NodeHttpHandler = (req: IncomingMessage, res: ServerResponse) => void;
// Edge / Fetch API handlertype FetchHandler = (request: Request) => Promise<Response>;
// Options accepted by http()interface HttpOptions { introspection?: boolean | { path?: string }; batch?: boolean | { path?: string; maxSize?: number }; cors?: boolean | CorsOptions;}Development
Section titled “Development”npx nx build http # compilenpx nx test http # run testsnpx nx lint http # lintPublishing
Section titled “Publishing”Build before publishing:
npx nx build httpFirst publish (scoped packages require --access public):
cd packages/httpnpm publish --access publicSubsequent releases — bump version in packages/http/package.json, then:
cd packages/httpnpm publish