Beignet API reference
    Preparing search index...

    Module @beignet/next

    @beignet/next

    Next.js adapter for the framework-agnostic @beignet/core/server runtime. It translates Next's Request/Response to/from the runtime's HttpRequestLike/HttpResponseLike shapes and provides small helpers for App Router handlers, OpenAPI routes, Swagger UI, and client base URLs.

    npm install @beignet/next next
    
    • @beignet/core/openapi for OpenAPI documentation
    • @beignet/core/ports if you want to define shared ports explicitly in your app

    This package requires TypeScript 5.0 or higher for proper type inference.

    // features/todos/contracts.ts
    import { createContractGroup } from "@beignet/core/contracts";
    import { z } from "zod";

    const todos = createContractGroup()
    .namespace("todos")
    .prefix("/api/todos");

    export const getTodo = todos
    .get("/:id")
    .pathParams(z.object({ id: z.string() }))
    .responses({ 200: z.object({
    id: z.string(),
    title: z.string(),
    completed: z.boolean(),
    }) });
    // server/index.ts
    import {
    createNextServer,
    defineRouteGroup,
    defineRoutes,
    } from "@beignet/next";
    import { getTodo } from "@/features/todos/contracts";

    export const server = await createNextServer({
    ports: {},
    routes: defineRoutes([
    {
    contract: getTodo,
    handle: async ({ path }) => ({
    status: 200,
    body: {
    id: path.id,
    title: "Example todo",
    completed: false,
    },
    }),
    },
    ]),
    createContext: async ({ req }) => {
    // DEMO ONLY: this reads an unauthenticated header to simulate identity.
    // Real applications should verify a signed token or session cookie first.
    return {
    userId: req.headers.get("x-user-id") || "anonymous",
    };
    },
    mapUnhandledError: ({ err }) => ({
    status: 500,
    body: {
    code: "INTERNAL_SERVER_ERROR",
    message: "Internal server error",
    ...(err instanceof Error ? { details: { message: err.message } } : {}),
    },
    }),
    });

    For larger apps, group related handlers near the feature and compose them with defineRoutes:

    const todoRoutes = defineRouteGroup({
    name: "todos",
    routes: [
    {
    contract: getTodo,
    handle: async ({ path }) => ({
    status: 200,
    body: {
    id: path.id,
    title: "Example todo",
    completed: false,
    },
    }),
    },
    ],
    });

    export const routes = defineRoutes([todoRoutes]);

    You have two options for routing:

    Register handlers centrally in server/index.ts, then expose the central handler from one catch-all Next.js route file:

    // app/api/[[...path]]/route.ts
    import { server } from "@/server";

    export const DELETE = server.api;
    export const GET = server.api;
    export const HEAD = server.api;
    export const OPTIONS = server.api;
    export const PATCH = server.api;
    export const POST = server.api;
    export const PUT = server.api;

    Create individual route files for each contract:

    // app/api/todos/[id]/route.ts
    import { server } from "@/server";
    import { getTodo } from "@/features/todos/contracts";

    export const GET = server
    .route(getTodo)
    .handle(async ({ ctx, path }) => {
    // Implement your handler logic
    const todo = await fetchTodoById(path.id);
    return {
    status: 200,
    body: todo,
    };
    });

    @beignet/next exposes the underlying web Request through HttpRequestLike. This lets handlers read raw bodies for webhooks while keeping the normal JSON contract flow for the rest of the app.

    export const POST = server.route(stripeWebhook).handle(async ({ req }) => {
    const rawBody = await req.text();
    const signature = req.headers.get("stripe-signature");

    verifyWebhookSignature(rawBody, signature);

    return { status: 200, body: { received: true } };
    });

    For downloads, plain text, and redirects, return a native web Response:

    export const GET = server.route(downloadFile).handle(async () =>
    new Response(await loadFile(), {
    headers: { "Content-Type": "application/octet-stream" },
    }),
    );

    export const GET = server.route(robotsTxt).handle(async () =>
    new Response("User-agent: *\nAllow: /\n", {
    headers: { "Content-Type": "text/plain; charset=utf-8" },
    }),
    );

    export const POST = server.route(startCheckout).handle(async () =>
    Response.redirect("https://checkout.example.com/session/123", 303),
    );

    Native Response instances intentionally bypass JSON serialization and response schema validation. Use { status, body } when you want Beignet to validate a JSON response; use Response when you want full transport control. Response-shaping hooks such as beforeSend only run for plain Beignet responses; observation hooks such as afterSend still receive the final status and headers.

    Creates a Next.js server instance with the given options.

    Parameters:

    • options: Same as createServer from @beignet/core/server:
      • ports: Required - Ports object defining available service interfaces
      • createContext: Async function to create request context
      • mapUnhandledError: Error handler function
      • routes?: Array of route configurations (contract + handler)
      • hooks?: Optional ordered server hooks
      • providers?: Optional array of service providers
      • providerEnv?: Optional environment variables for providers
      • providerConfig?: Optional provider configuration overrides

    Returns: Promise<NextServer<Ctx>>

    A Next.js handler for routes registered in server/index.ts. Framework-style apps usually expose it once from a catch-all API route.

    // app/api/[[...path]]/route.ts
    import { server } from "@/server";

    export const DELETE = server.api;
    export const GET = server.api;
    export const HEAD = server.api;
    export const OPTIONS = server.api;
    export const PATCH = server.api;
    export const POST = server.api;
    export const PUT = server.api;

    Returns a route builder for creating custom handlers for a specific contract. The contract is registered globally and available via server.api.

    Returns: Route builder with:

    • handle(fn): Create a custom handler function
    // app/api/todos/[id]/route.ts
    import { server } from "@/server";
    import { getTodo } from "@/features/todos/contracts";
    import { getTodoUseCase } from "@/features/todos/use-cases/get-todo";

    // Option 1: Custom handler
    export const GET = server
    .route(getTodo)
    .handle(async ({ req, ctx, path, query, body }) => {
    // Your implementation
    return { status: 200, body: { id: path.id, title: "..." } };
    });

    // Option 2: Call a use case inside the handler
    export const GET = server
    .route(getTodo)
    .handle(async ({ ctx, path }) => {
    const todo = await getTodoUseCase.run({
    ctx,
    input: { id: path.id },
    });

    return { status: 200, body: todo };
    });

    Creates a context object from Next.js Server Components by automatically extracting headers and cookies. This allows you to call use cases directly from React Server Components without going through API routes.

    Returns: Promise<Ctx> - Your context object from createContext

    // app/my-page/page.tsx
    import { server } from "@/server";
    import { getTodoUseCase } from "@/features/todos/use-cases/get-todo";

    export default async function MyPage() {
    // Create context from Next.js headers and cookies
    const ctx = await server.createContextFromNext();

    // Call use case directly
    const todo = await getTodoUseCase.run({
    ctx,
    input: { id: "123" }
    });

    return <div>{todo.title}</div>;
    }

    This method:

    • Automatically calls Next.js's headers() and cookies() functions
    • Creates a minimal Request-like object with headers and cookies access
    • Calls your createContext function with this request object
    • Returns the same context type you get in API route handlers
    • Uses the HTTP method "GET" for the internal Request-like object. If your createContext implementation inspects req.method, it will always see "GET" when invoked via createContextFromNext().
    • The req.url is set to a placeholder (http://core/server-component.invalid) since Server Components don't have real HTTP URLs
    • The req.json() and req.text() methods return empty values since there's no actual HTTP request body in Server Components

    Note: This method can only be called from Next.js Server Components (not in Client Components or during build time).

    Stops the server and cleans up resources (closes provider connections, etc.).

    await server.stop();
    

    When using .handle(), your handler function receives an object with:

    {
    req: HttpRequestLike, // Raw request object
    ctx: Ctx, // Your custom context from createContext
    path: PathParams, // Validated path parameters
    query: QueryParams, // Validated query parameters
    body: Body, // Validated request body
    contract: Contract, // Resolved contract metadata and schemas
    }

    Beignet promotes clean architecture by separating use cases from HTTP concerns. Call use cases from handlers so the HTTP layer stays explicit:

    // features/todos/use-cases/get-todo.ts
    export async function getTodoUseCase(
    input: { id: string },
    ports: AppPorts
    ) {
    return await ports.db.todos.findById(input.id);
    }

    // app/api/todos/[id]/route.ts
    export const GET = server
    .route(getTodo)
    .handle(async ({ ctx, path }) => {
    const todo = await getTodoUseCase({ id: path.id }, ctx.ports);

    return { status: 200, body: todo };
    });

    Hooks can be added at the server level:

    import { createNextServer } from "@beignet/next";
    import { createLoggingHooks } from "@beignet/core/server";

    const logging = createLoggingHooks({
    logger: console,
    requestIdHeader: "x-request-id",
    });

    export const server = await createNextServer({
    ports: {},
    hooks: [logging],
    createContext: async () => ({}),
    mapUnhandledError: () => ({
    status: 500,
    body: {
    code: "INTERNAL_SERVER_ERROR",
    message: "Internal server error",
    },
    }),
    });

    If you have @beignet/core/openapi installed, use createOpenAPIHandler for a Next.js route. The handler infers the current request origin and adds it as the OpenAPI server by default.

    // app/api/openapi/route.ts
    import { createOpenAPIHandler } from "@beignet/next";
    import { server } from "@/server";

    export const GET = createOpenAPIHandler(server.contracts, {
    title: "My API",
    version: "1.0.0",
    });

    server.contracts is populated from contracts registered through createNextServer({ routes }). If you export per-file Next handlers with server.route(contract).handle(...), keep an explicit contract list or exported route registry for OpenAPI because those route files are not imported by the server automatically.

    You can also serve Swagger UI without writing the HTML route by hand:

    // app/api/docs/route.ts
    import { createSwaggerUIHandler } from "@beignet/next";

    export const GET = createSwaggerUIHandler({
    title: "My API Documentation",
    specUrl: "/api/openapi",
    });

    Use createStorageRoute to serve public objects from a StoragePort in a Next.js App Router route. The route streams object bodies and maps missing objects, private objects, invalid keys, and paths outside basePath to 404.

    // app/storage/[...key]/route.ts
    import { createStorageRoute } from "@beignet/next";
    import { server } from "@/server";

    export const { GET, HEAD } = createStorageRoute(server.ports.storage, {
    basePath: "/storage",
    });

    Served responses preserve object Content-Type, Cache-Control, Content-Length, and Last-Modified headers when available.

    Use createUploadRoute to expose a Beignet upload router from a focused App Router route:

    // app/api/uploads/[uploadName]/[action]/route.ts
    import { createUploadRouter } from "@beignet/core/uploads";
    import { createUploadRoute } from "@beignet/next";
    import { postUploads } from "@/features/posts/uploads";
    import { server } from "@/server";

    const uploadRouter = createUploadRouter({
    uploads: postUploads,
    ctx: () => server.createContextFromNext(),
    storage: server.ports.storage,
    instrumentation: server.ports.devtools,
    });

    export const { POST } = createUploadRoute(uploadRouter);

    The action segment must be prepare, upload, or complete.

    Use createNextClient when a client may run in both browser and server environments. Browser calls default to same-origin relative URLs. Server calls use NEXT_PUBLIC_API_URL, then VERCEL_URL, then http://localhost:${PORT || 3000}.

    // client/api-client.ts
    import { createNextClient } from "@beignet/next";

    export const apiClient = createNextClient({
    headers: async () => ({}),
    });

    If your local app runs on a non-default port, provide a server-only fallback:

    export const apiClient = createNextClient({
    serverBaseUrl: () => `http://localhost:${process.env.PORT || 3002}`,
    });

    For deployed apps, prefer setting NEXT_PUBLIC_API_URL when API calls should target a different origin.

    Providers are service adapters that implement ports (database, cache, logger, etc.):

    import { createNextServer } from "@beignet/next";
    import { createDrizzleTursoProvider } from "@beignet/provider-drizzle-turso";
    import { loggerPinoProvider } from "@beignet/provider-logger-pino";
    import * as schema from "@/db/schema";

    const drizzleTursoProvider = createDrizzleTursoProvider({ schema });

    export const server = await createNextServer({
    ports: {},
    providers: [
    drizzleTursoProvider,
    loggerPinoProvider,
    ],
    providerEnv: process.env,
    createContext: async ({ ports }) => ({
    // Access providers via ports
    db: ports.db,
    logger: ports.logger,
    }),
    mapUnhandledError: () => ({
    status: 500,
    body: {
    code: "INTERNAL_SERVER_ERROR",
    message: "Internal server error",
    },
    }),
    });
    export const server = await createNextServer({
    ports: {},
    createContext: async () => ({}),
    mapUnhandledError: ({ err, ctx }) => {
    console.error("Unhandled error:", err);
    return {
    status: 500,
    body: {
    code: "INTERNAL_SERVER_ERROR",
    message: "Internal server error",
    ...(process.env.NODE_ENV === "development" && err instanceof Error
    ? { error: err.message }
    : {}),
    },
    };
    },
    });

    Declare expected business failures on the contract with .errors(...), then throw your app's catalog helper from handlers or use cases.

    import { appError } from "@/features/shared/errors";

    export const GET = server
    .route(getTodo)
    .handle(async ({ ctx, path }) => {
    const todo = await fetchTodoById(path.id);

    if (!todo) {
    throw appError("TodoNotFound", { details: { id: path.id } });
    }

    return { status: 200, body: todo };
    });

    Creates a @beignet/core/client instance with Next.js-friendly base URL defaults.

    Resolves the base URL used by createNextClient.

    Creates a Next.js route handler that returns an OpenAPI 3.1 JSON document. Requires @beignet/core/openapi in the app.

    When you use central route registration, prefer server.contracts or contractsFromRoutes(routes) so OpenAPI is generated from the same route list used by the runtime.

    Creates a Next.js route handler that serves Swagger UI for an OpenAPI endpoint.

    Converts a Next.js Request to the framework-agnostic HttpRequestLike shape.

    Converts an HttpResponseLike to a Next.js Response.

    These are used internally by the adapter but can be used directly if needed.

    // features/todos/contracts.ts
    import { createContractGroup } from "@beignet/core/contracts";
    import { z } from "zod";

    const todos = createContractGroup()
    .namespace("todos")
    .prefix("/api/todos");

    const todoSchema = z.object({
    id: z.string(),
    title: z.string(),
    completed: z.boolean(),
    });

    export const listTodos = todos
    .get("/")
    .responses({ 200: z.array(todoSchema) });

    export const getTodo = todos
    .get("/:id")
    .pathParams(z.object({ id: z.string() }))
    .responses({ 200: todoSchema });

    export const createTodo = todos
    .post("/")
    .body(z.object({ title: z.string() }))
    .responses({ 201: todoSchema });

    export const updateTodo = todos
    .put("/:id")
    .pathParams(z.object({ id: z.string() }))
    .body(z.object({ title: z.string(), completed: z.boolean() }))
    .responses({ 200: todoSchema });

    export const deleteTodo = todos
    .delete("/:id")
    .pathParams(z.object({ id: z.string() }))
    .responses({ 204: null });

    // server/index.ts
    import { createNextServer, defineRoutes } from "@beignet/next";
    import * as todosContracts from "@/features/todos/contracts";

    export const server = await createNextServer({
    ports: {},
    routes: defineRoutes([
    { contract: todosContracts.listTodos, handle: async () => ({ status: 200, body: [] }) },
    { contract: todosContracts.getTodo, handle: async ({ path }) => ({ status: 200, body: { id: path.id, title: "...", completed: false } }) },
    { contract: todosContracts.createTodo, handle: async ({ body }) => ({ status: 201, body: { id: "1", ...body, completed: false } }) },
    { contract: todosContracts.updateTodo, handle: async ({ path, body }) => ({ status: 200, body: { id: path.id, ...body } }) },
    { contract: todosContracts.deleteTodo, handle: async () => ({ status: 204 }) },
    ]),
    createContext: async () => ({ todos: [] }),
    mapUnhandledError: () => ({
    status: 500,
    body: {
    code: "INTERNAL_SERVER_ERROR",
    message: "Internal server error",
    },
    }),
    });
    // app/api/[[...path]]/route.ts
    import { server } from "@/server";

    export const DELETE = server.api;
    export const GET = server.api;
    export const HEAD = server.api;
    export const OPTIONS = server.api;
    export const PATCH = server.api;
    export const POST = server.api;
    export const PUT = server.api;
    // server/index.ts
    import { createNextServer } from "@beignet/next";
    import { AuthUnauthorizedError } from "@beignet/core/ports";
    import { getTodo } from "@/features/todos/contracts";

    export const server = await createNextServer({
    ports: {},
    createContext: async ({ req }) => {
    const user = await getUserFromRequest(req);

    if (!user) {
    throw new AuthUnauthorizedError();
    }

    return { user };
    },
    mapUnhandledError: () => {
    return {
    status: 500,
    body: {
    code: "INTERNAL_SERVER_ERROR",
    message: "Internal server error",
    },
    };
    },
    });

    You can call use cases directly from React Server Components using createContextFromNext():

    // app/todos/page.tsx
    import { server } from "@/server";
    import { listTodosUseCase } from "@/features/todos/use-cases/list-todos";

    export default async function TodosPage() {
    // Create context from Next.js runtime
    const ctx = await server.createContextFromNext();

    // Call use case directly - no API route needed!
    const result = await listTodosUseCase.run({
    ctx,
    input: { limit: 10, offset: 0 }
    });

    return (
    <div>
    <h1>Todos</h1>
    <ul>
    {result.items.map(todo => (
    <li key={todo.id}>{todo.title}</li>
    ))}
    </ul>
    </div>
    );
    }

    This approach:

    • Eliminates unnecessary API routes for server-side data fetching
    • Maintains type safety and business logic separation
    • Automatically handles headers and cookies from Next.js
    • Reuses your existing context creation logic

    MIT

    Interfaces

    NextServer
    ServerInstance

    Type Aliases

    CreateOpenAPIHandlerOptions
    CreateServerOptions
    CreateStorageRouteOptions
    CreateSwaggerUIHandlerOptions
    CreateUploadRouteOptions
    NextClientConfig
    OpenAPIServer
    RouteGroup

    Functions

    contractsFromRoutes
    createNextClient
    createNextServer
    createOpenAPIHandler
    createServer
    createStorageRoute
    createSwaggerUIHandler
    createUploadRoute
    defineRouteGroup
    defineRoutes
    resolveNextBaseUrl
    toNextResponse
    toRequestLike