npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@beignet/next

v0.0.3

Published

Next.js server-side handlers for Beignet

Readme

@beignet/next

Next.js adapter for the framework-agnostic @beignet/core/server runtime. It builds on @beignet/web for standard Request/Response handling and adds Next-specific helpers for App Router handlers, Server Component context, OpenAPI routes, Swagger UI, uploads, outbox drains, storage routes, and client base URLs.

Installation

npm install @beignet/next next

Optional add-ons

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

TypeScript requirements

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

Quick start

1. Define your contracts

// 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(),
  }) });

2. Create your server

// 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]);

3. Set up routes

You have two options for routing:

Option A: catch-all framework route (recommended)

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;

Option B: per-contract routes

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,
    };
  });

Raw requests and non-JSON responses

@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.

API reference

createNextServer<Ctx>(options)

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>>

NextServer methods

server.api

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;

server.route(contract)

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 };
  });

server.createContextFromNext()

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).

server.stop()

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

await server.stop();

Handler function context

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
}

Use case integration

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

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",
    },
  }),
});

OpenAPI documentation

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",
});

Public storage routes

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.

Upload routes

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.

Outbox drain routes

Use createOutboxDrainRoute to expose durable outbox delivery from a cron or scheduled serverless route. The helper requires a bearer secret, drains one bounded batch with @beignet/core/outbox, records a devtools event when available, and returns a JSON summary.

// app/api/cron/outbox/drain/route.ts
import { createOutboxDrainRoute } from "@beignet/next";
import { env } from "@/lib/env";
import { server } from "@/server";
import { outboxRegistry } from "@/server/outbox";

export const runtime = "nodejs";

export const { GET, POST } = createOutboxDrainRoute({
  server,
  registry: outboxRegistry,
  secret: env.CRON_SECRET,
  batchSize: 100,
});

Export both GET and POST when you want the route to work with schedulers that call either method. Export only the method your scheduler uses when you want a narrower route surface.

Call the route from your scheduler with:

Authorization: Bearer <CRON_SECRET>

Do not start long-running outbox polling loops from provider lifecycle hooks in serverless apps.

Client creation

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

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",
    },
  }),
});

Error handling

Global error handler

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 }
          : {}),
      },
    };
  },
});

Route-level error handling

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 };
  });

Helper functions

createNextClient(config?): Client

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

resolveNextBaseUrl(config?): string

Resolves the base URL used by createNextClient.

createOpenAPIHandler(contracts, options): (req: Request) => Promise<Response>

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.

createSwaggerUIHandler(options?): (req: Request) => Response

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

toRequestLike(req: Request): HttpRequestLike

Re-export from @beignet/web. Converts a standard Request to the framework-agnostic HttpRequestLike shape.

toWebResponse(res: HttpResponseLike): Response

Re-export from @beignet/web. Converts an HttpResponseLike to a standard Response.

toNextResponse(res: HttpResponseLike): Response

Next-named alias for toWebResponse(...).

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

Examples

Basic CRUD API

// 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;

With authentication

// 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",
      },
    };
  },
});

Server component usage

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

Related packages

License

MIT