@beignet/web
Beignet is experimental alpha software. The
0.0.xpackage 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.
Installation
npm install @beignet/web @beignet/coreQuick start
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,
};Bun
import { server } from "./server";
Bun.serve({
fetch: server.fetch,
});Lower-level helpers
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 standardRequestto Beignet's framework-neutral request shape.toWebResponse(response)converts a Beignet response to a standardResponse.webFetchAdapteris the concreteHttpAdapter<Request, Response>implementation for Web Fetch runtimes.
Native Response instances returned by handlers are passed through unchanged.
Plain Beignet responses are serialized as JSON unless the body is undefined
or null.
Adapter contract
@beignet/core/server owns framework behavior: route matching, hooks, request
validation, response validation, error mapping, response ownership, and provider
lifecycle. @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.
Testing
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: appPorts,
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:
onUnboundPortsdefaults to"ignore", so apps with deferred provider ports boot without installing every provider. Reading an unbound port still throws on use. Production servers created withcreateFetchServer(...)keep the strict"error"default.mapUnhandledErrordefaults 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.
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 });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" },
});