Beignet API reference
    Preparing search index...

    Module @beignet/web

    @beignet/web

    Runtime: Beignet requires Node.js 22.12 or newer. Bun is optional.

    Caution

    Beignet is experimental alpha software. The 0.0.x package line is for early evaluation, and APIs may change between releases while the framework settles.

    Web Fetch adapter for Beignet's framework runtime.

    Use this package when your runtime accepts a standard Request and returns a standard Response, such as Cloudflare Workers, Bun, Deno, Node fetch servers, or tests that should avoid a framework-specific adapter.

    npm install @beignet/web @beignet/core
    

    This package ships the TanStack Intent skill @beignet/web#fetch-server. Load it when composing a Web Fetch runtime, configuring the CLI web profile, mounting Bun or serverless handlers, returning native responses, adding raw routes, or testing through @beignet/web/testing.

    import { createFetchServer } from "@beignet/web";
    import { routes } from "./server/routes";

    export const server = await createFetchServer({
    ports,
    routes,
    context: ({ ports, requestId }) => ({
    requestId,
    ports,
    }),
    mapUnhandledError: () => ({
    status: 500,
    body: {
    code: "INTERNAL_SERVER_ERROR",
    message: "Internal server error",
    },
    }),
    });

    export default {
    fetch: server.fetch,
    };
    import { server } from "./server";

    Bun.serve({
    fetch: server.fetch,
    });

    Declare context.service when queues, schedules, tasks, seeds, or other non-HTTP work needs the same assembled app context as requests. Use server.runServiceContext(input?, fn) in seeds and one-off scripts so the ambient correlation frame is scoped to the callback:

    await server.runServiceContext(async (ctx) => {
    await runSeeds({ seeds, ctx });
    });

    server.createServiceContext(input?) remains available to long-lived servers, workers, and test processes that own the surrounding async lifetime.

    server.rawRoute({ name, method, path, metadata }).handle(fn) builds a Fetch handler for a route that cannot be a contract — third-party callbacks, signature-verified webhooks, streaming endpoints — that still runs the whole server pipeline: hooks, context creation, instrumentation, and framework error mapping. Request parsing is skipped and the body stays unconsumed for the handler; metadata feeds metadata-driven hooks such as rate limiting exactly like contract metadata. Raw routes are not added to the route registry — mount the returned handler at the route's own path.

    For Server-Sent Events, return createServerSentEventResponse(...) from @beignet/core/server. The helper owns portable SSE framing, JSON encoding, heartbeats, request abort through req.signal, subscription cleanup, a 1 MiB unread-data limit, an optional maximum connection lifetime, and anti-buffering headers. The start(...) callback receives a stream-scoped signal for cancellable asynchronous setup; expected AbortError rejections caused by stream closure are treated as cancellation. The app still owns authorization, replay, connection limits, and reconciliation.

    import {
    createFetchHandler,
    toRequestLike,
    toWebResponse,
    webFetchAdapter,
    } from "@beignet/web";
    • createFetchHandler(server) adapts a Beignet server instance or API handler to (req: Request) => Promise<Response>.
    • toRequestLike(req) converts a standard Request to Beignet's framework-neutral request shape, preserving the native request and its abort signal.
    • toWebResponse(response) converts a Beignet response to a standard Response, appending every item in an array-valued response header as a separate field. Use this for repeated fields such as Set-Cookie.
    • webFetchAdapter is the concrete HttpAdapter<Request, Response> implementation for Web Fetch runtimes.

    Native Response instances returned by handlers are passed through unchanged. An explicitly returned ReadableStream is also passed to the native response unchanged, including for streamed application/json and structured +json responses. Other plain Beignet responses are serialized as JSON when Content-Type is absent, application/json, or a structured +json media type. To intentionally return text or binary data, set an explicit non-JSON Content-Type and return a Web BodyInit value:

    return {
    status: 200,
    headers: { "content-type": "text/plain; charset=utf-8" },
    body: "hello",
    };

    Use a native Response when transport-specific behavior such as redirects or multipart boundary generation needs direct Web API control.

    @beignet/core/server owns framework behavior: route matching, hooks, request validation, response parsing, error mapping, response ownership, and provider lifecycle. For framework-neutral route responses, the parsed response-schema output becomes the body that @beignet/web serializes. @beignet/web owns only the Web Fetch edge conversion.

    The exported webFetchAdapter implements the formal core adapter contract:

    import type { HttpAdapter } from "@beignet/core/server";

    export const adapter: HttpAdapter<Request, Response> = webFetchAdapter;

    Use the same shape for a future runtime adapter with non-Fetch native request or response types.

    Use @beignet/web/testing to exercise routes through the same Web Fetch adapter without opening a network port. Use @beignet/core/testing beside it when the route needs an app-style context and test ports.

    import { createTestContextFactory, createTestPorts } from "@beignet/core/testing";
    import { createTestApp } from "@beignet/web/testing";
    import { getTodo } from "./features/todos/contracts";
    import { routes } from "./server/routes";

    const fixture = createTestPorts<AppContext["ports"]>({
    base: initialPorts,
    overrides: { todos: createInMemoryTodoRepository() },
    });
    const createContext = createTestContextFactory<AppContext, AppContext["ports"]>({
    ports: fixture.ports,
    });
    const app = await createTestApp({
    ports: fixture.ports,
    routes,
    context: () => createContext(),
    });

    const todo = await app.request(getTodo, {
    path: { id: "todo_1" },
    });

    app.request(contract, args) uses the same typed call arguments as @beignet/core/client, while app.safeRequest(contract, args) returns a typed success/error result instead of throwing.

    createTestApp(...) applies two test-friendly defaults, and an explicit option always wins:

    • onUnboundPorts defaults to "ignore", so apps with deferred provider ports boot without installing every provider. Reading an unbound port still throws on use. Production servers created with createFetchServer(...) keep the strict "error" default.
    • mapUnhandledError defaults to a mapper that returns { status: 500, body: { code: "INTERNAL_SERVER_ERROR", message: err.message } }, so failing tests show the real error message instead of a generic response.

    Metadata-driven behavior needs both halves wired: a contract's metadata.rateLimit or metadata.idempotency only takes effect when the matching hook (createRateLimitHooks(...) / createIdempotencyHooks(...)) is in the test app's hooks and its port is bound. createTestApp(...) warns at creation when registered contracts declare behavior the app cannot enforce, so "the 429 never happened" fails loudly instead of silently passing. createTestPorts(...) from @beignet/core/testing already binds memory rateLimit and idempotency ports, so passing the app's hooks is usually the only missing piece:

    const app = await createTestApp({
    ports: fixture.ports,
    context: appContext,
    trustedProxy: { clientIp: "x-forwarded-for-last" },
    hooks: [
    createRateLimitHooks(),
    createIdempotencyHooks(),
    ],
    routes,
    });

    // Declared limits now produce real 429s...
    const responses = await Promise.all(
    Array.from({ length: 61 }, () => app.fetch("http://beignet.test/api/search")),
    );
    expect(responses.at(-1)?.status).toBe(429);

    // ...and repeated idempotency keys replay instead of re-running the handler.
    const replay = await app.fetch("http://beignet.test/api/todos", {
    method: "POST",
    headers: { "idempotency-key": "todo-1", "content-type": "application/json" },
    body: "{}",
    });
    expect(replay.headers.get("idempotency-replayed")).toBe("true");

    Apps that declare their context blueprint with defineServerContext(...) in server/context.ts can pass the same value to both the runtime server and createTestApp(...):

    import { appContext } from "../server/context";

    const app = await createTestApp({ ports, routes, context: appContext });

    The underlying app.server retains the blueprint's service-input type, so tests can call app.server.runServiceContext(input?, fn) for non-HTTP work.

    Use createTestRequester(...) when a test suite repeats the same auth, correlation, or request-shaping headers:

    import { createTestApp, createTestRequester } from "@beignet/web/testing";

    const app = await createTestApp({ ports, routes, context: createContext });
    const authedRequest = createTestRequester(app, {
    headers: {
    "x-user-id": "user_1",
    "x-request-id": "req_1",
    },
    });

    const todo = await authedRequest.request(getTodo, {
    path: { id: "todo_1" },
    });

    createFetchServer(...) and createTestApp(...) accept the same server-level trustedProxy policy. The request context factory and server hooks receive one resolved requestInfo; forwarding headers remain untrusted when the option is omitted.

    testing