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

v0.0.3

Published

Core framework primitives for Beignet

Readme

@beignet/core

Core framework primitives for Beignet

This package provides Beignet's framework primitives: contracts, server runtime, typed client, use cases, ports, domain helpers, app errors, config, events, idempotency, outbox, mail, notifications, schedules, uploads, pagination helpers, testing helpers, and OpenAPI generation.

Installation

npm install @beignet/core

# Use with your preferred Standard Schema library
npm install zod
# or
npm install valibot
# or
npm install arktype

TypeScript requirements

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

Subpaths

Install @beignet/core once, then import the framework area you need. The package intentionally has no root entrypoint; use explicit subpaths so imports name the framework area they depend on.

| Import path | Responsibility | | --- | --- | | @beignet/core/application | Use case builder and test helpers | | @beignet/core/client | Typed HTTP client | | @beignet/core/config | Environment config validation | | @beignet/core/contracts | HTTP contract builders, types, path helpers, and contract metadata | | @beignet/core/domain | Entities, value objects, and domain events | | @beignet/core/errors | Error catalogs and response helpers | | @beignet/core/events | Events and listeners | | @beignet/core/idempotency | Retry-safe command, webhook, and job primitives | | @beignet/core/jobs | Job definitions, retry policies, and inline job dispatch | | @beignet/core/mail | Mail port and memory mailer | | @beignet/core/notifications | Notification definitions, dispatchers, mail channels, and test adapters | | @beignet/core/openapi | OpenAPI generation | | @beignet/core/outbox | Durable event and job outbox | | @beignet/core/pagination | Offset/cursor page types, normalizers, and result helpers | | @beignet/core/ports | App-facing ports, auth, audit, policies, cache, storage, logging, and redaction | | @beignet/core/ports/testing | Port and policy test helpers | | @beignet/core/providers | Provider lifecycle and instrumentation primitives | | @beignet/core/schedules | Scheduled task primitives | | @beignet/core/server | Framework-agnostic server runtime and hook helpers | | @beignet/core/testing | Test factories, seed definitions, and seed runners | | @beignet/core/uploads | Upload definitions, router, signer port, and test signer | | @beignet/core/uploads/client | Browser upload client for server and direct uploads |

Key concepts

Contract

A contract is the single source of truth for an API endpoint. It describes:

  • HTTP method and path (with path parameters)
  • Path parameters, query parameters, request headers, and request body schemas
  • Response schemas (per status code, including error responses)
  • Metadata for auth, rate limiting, idempotency, etc.

Contract group

A contract group allows you to share configuration across related endpoints, such as a common namespace, route metadata, headers, and shared response schemas.

Usage

Defining contracts

import { z } from "zod";
import { createContractGroup } from "@beignet/core/contracts";

// Create a contract group for related endpoints
const todos = createContractGroup()
  .namespace("todos")
  .prefix("/api/todos")
  .meta({ auth: "required" })
  .headers(z.object({
    authorization: z.string().startsWith("Bearer "),
  }));

// Define schemas
const TodoSchema = z.object({
  id: z.string(),
  title: z.string(),
  completed: z.boolean(),
});

const CreateTodoRequest = z.object({
  title: z.string().min(1),
  completed: z.boolean().optional(),
});

// Define contracts
export const getTodo = todos
  .get("/:id")
  .pathParams(z.object({ id: z.string() }))
  .responses({ 200: TodoSchema })
  .errors({
    TodoNotFound: {
      code: "TODO_NOT_FOUND",
      status: 404,
      message: "Todo not found",
      details: z.object({ id: z.string() }),
    },
  });

export const createTodo = todos
  .post("/")
  .body(CreateTodoRequest)
  .responses({ 201: TodoSchema });

export const listTodos = todos
  .get("/")
  .query(z.object({ 
    completed: z.boolean().optional(),
    limit: z.coerce.number().optional(),
  }))
  .responses({ 200: z.array(TodoSchema) });

Clients and OpenAPI generation infer required path argument keys from literal path templates. Use .pathParams(...) when you want runtime validation, coercion, richer OpenAPI schemas, or parameter descriptions.

Use .headers(...) for request headers that are part of the endpoint contract. Declare header keys in lowercase; server and client runtime matching is case-insensitive.

Request bodies are supported for POST, PUT, and PATCH contracts only.

If you do not pass name, Beignet generates one from the HTTP method and full path:

createContract({ method: "GET", path: "/users/:id" }).name;
// "getUsersById"

createContract({ method: "POST", path: "/api/todos" }).name;
// "createTodos"

Auto-generated names ignore a leading /api segment, include path parameters as By..., and are used as defaults in places like React Query keys and OpenAPI operationIds. Pass name explicitly when you need a custom stable identifier.

Path prefixes

Use .prefix(...) on a contract group to compose shared URL path segments without repeating them on every route:

const api = createContractGroup().prefix("/api/v1");

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

export const listTodos = todos.get("/");
// GET /api/v1/todos

export const getTodo = todos.get("/:id");
// GET /api/v1/todos/:id

Prefixes compose immutably and normalize boundary slashes. namespace() controls resource identity for contract names, OpenAPI tags, and client cache grouping; prefix() only controls URL paths.

Test factories and seeds

Use @beignet/core/testing to keep feature tests and demo seed data port-based. Factories build app-owned records, and optional persist functions write through the context you pass in:

import {
  defineFactory,
  defineSeed,
  resetFactories,
  runSeeds,
} from "@beignet/core/testing";

const postFactory = defineFactory("post", {
  defaults: ({ sequence }) => ({
    title: `Post ${sequence}`,
    content: "Created in a test.",
  }),
  persist: (ctx: AppContext, post) => ctx.ports.posts.create(post),
});

const demoPostsSeed = defineSeed("demo-posts", {
  run: async (ctx: AppContext) => {
    await postFactory.createList(ctx, 3);
  },
});

export async function seedDemoPosts(ctx: AppContext) {
  await runSeeds({ ctx, seeds: [demoPostsSeed] });
}

export function resetPostFactories() {
  resetFactories(postFactory);
}

Keep factories and seeds app-owned. They should not import database clients, ORM table objects, or provider SDKs directly.

Pagination

Use @beignet/core/pagination to keep list use cases and repository ports consistent without coupling them to an ORM:

import { normalizeOffsetPage } from "@beignet/core/pagination";

const page = normalizeOffsetPage(input, {
  defaultLimit: 20,
  maxLimit: 100,
});

return ctx.ports.posts.findMany({
  page,
  filters: { status: input.status },
  sort: { field: "createdAt", direction: "desc" },
});

Beignet's convention is items for list contents and page for pagination metadata. Keep filters and sort options app-owned plain objects.

Idempotency

Use @beignet/core/idempotency when a command, webhook, or job may be retried and must not perform duplicate work:

import {
  createIdempotencyFingerprint,
  runIdempotently,
} from "@beignet/core/idempotency";

const result = await runIdempotently(ctx.ports.idempotency, {
  namespace: "todos.create",
  key: input.idempotencyKey,
  scope: {
    tenantId: ctx.tenant?.id,
    actorId: ctx.actor?.id,
  },
  fingerprint: await createIdempotencyFingerprint(input, {
    omit: ["idempotencyKey"],
  }),
  ttlSec: 60 * 60 * 24,
  run: () => ctx.ports.uow.transaction((tx) => tx.todos.create(input)),
});

The memory store is useful for tests and local examples:

import { createMemoryIdempotencyStore } from "@beignet/core/idempotency";

const idempotency = createMemoryIdempotencyStore();

Production apps should back IdempotencyPort with atomic SQL or Redis storage. For high-integrity workflows, prefer exposing a transaction-scoped tx.idempotency port from the app Unit of Work so reservation, business writes, audit records, domain-event records, and idempotency completion commit together.

Outbox

Use @beignet/core/outbox when events or jobs must be recorded in the same database transaction as the business write, then delivered later with retries:

import {
  createOutboxEventRecorder,
  defineOutboxRegistry,
  drainOutbox,
} from "@beignet/core/outbox";
import {
  createDrizzleTursoOutboxPort,
  createDrizzleTursoUnitOfWork,
} from "@beignet/provider-drizzle-turso";

const uow = createDrizzleTursoUnitOfWork({
  db,
  createTransactionPorts: (tx) => {
    const outbox = createDrizzleTursoOutboxPort(tx);

    return {
      posts: createPostRepository(tx),
      events: createOutboxEventRecorder(outbox),
      outbox,
    };
  },
});

const registry = defineOutboxRegistry({
  events: [PostPublished],
  jobs: [SendPostPublishedEmailJob],
});

await drainOutbox({
  outbox: ctx.ports.outbox,
  registry,
  eventBus: ctx.ports.eventBus,
  jobs: ctx.ports.jobs,
});

The outbox is at-least-once delivery. Use idempotent listeners or jobs when a duplicate delivery would be harmful.

Contract metadata and route hooks

Use metadata to describe cross-cutting concerns for OpenAPI, clients, docs, and app conventions:

const sendMessage = messages
  .post("/api/messages")
  .body(SendMessageRequest)
  .responses({ 201: SendMessageResponse })
  .meta({
    auth: "required",
    idempotency: {
      required: true,
      header: "idempotency-key",
      scope: "actor-tenant",
      ttlSec: 300,
    },
    rateLimit: {
      max: 60,
      windowSec: 60,
      scope: "user",
    },
  });

Use route hooks for runtime enforcement where the route is wired:

import {
  createAuthHooks,
  defineRoute,
  defineRouteGroup,
} from "@beignet/core/server";

const auth = createAuthHooks<AppContext, { user: CurrentUser }>({
  resolve: async ({ ctx, req }) => {
    const session = await ctx.ports.auth.getSession(req);

    return session ? { user: session.user } : null;
  },
});

const route = defineRoute<AppContext>();

export const messageRoutes = defineRouteGroup<AppContext>()({
  name: "messages",
  routes: [
    route({
      contract: sendMessage,
      hooks: [auth.required()],
      handle: async ({ ctx, body }) => {
        ctx.user.id;

        return sendMessageUseCase.run({ ctx, input: body });
      },
    }),
  ],
});

OpenAPI metadata

Add OpenAPI-specific metadata for documentation using the .openapi() method:

export const getTodo = todos
  .get("/api/todos/:id")
  .pathParams(z.object({ id: z.string() }))
  .responses({ 200: TodoSchema })
  .openapi({
    summary: "Get a todo by ID",
    description: "Retrieves a single todo item by its unique identifier",
    tags: ["todos"],
    deprecated: false,
    operationId: "getTodoById",
    externalDocs: {
      url: "https://docs.example.com/todos",
      description: "Todo documentation",
    },
    security: [{ bearerAuth: [] }],
  });

Schema introspection

Contracts expose their schemas for runtime introspection:

getTodo.schema.pathParams;  // Path parameter schema
getTodo.schema.query;      // Query parameter schema
getTodo.schema.body;       // Request body schema
getTodo.schema.responses;  // Response schemas by status code
getTodo.path;              // "/api/todos/:id"
getTodo.pathTemplate;      // "/api/todos/:id" (alias)
getTodo.method;            // "GET"
getTodo.metadata;          // { auth: "required", ... }

API reference

createContractGroup()

Creates a new contract group for defining related endpoints.

const group = createContractGroup()
  .namespace("myNamespace")    // Optional resource namespace
  .prefix("/api/v1")           // Optional URL path prefix
  .meta({ auth: "required" })  // Shared metadata
  .headers(AuthHeaders)         // Shared request headers
  .errors({                     // Shared catalog errors
    TenantSuspended: errors.TenantSuspended,
  });

Any non-empty response map is treated as a response contract. Include successful statuses such as 200 or 201 alongside custom error statuses; use responses: {} only when you want to skip response validation. Prefer .errors(...) for expected business failures that should use Beignet's standard error envelope.

Contract builder methods

| Method | Description | |--------|-------------| | .get(path) | Define a GET endpoint | | .post(path) | Define a POST endpoint | | .put(path) | Define a PUT endpoint | | .patch(path) | Define a PATCH endpoint | | .delete(path) | Define a DELETE endpoint | | .pathParams(schema) | Define path parameter schema | | .query(schema) | Define query parameter schema | | .headers(schema) | Define request header schema | | .body(schema) | Define request body schema | | .responses({ ... }) | Define or merge response schemas by status code | | .errors({ ... }) | Declare route-owned catalog errors using Beignet's standard error envelope | | .meta(metadata) | Add custom metadata | | .openapi(options) | Add OpenAPI metadata (summary, tags, etc.) |

Standard Schema support

This package works with any Standard Schema compatible library:

  • Zod - Most popular, excellent TypeScript inference
  • Valibot - Lightweight alternative to Zod
  • ArkType - High-performance runtime validation

OpenAPI generation currently requires Zod schemas, even though core contracts can use any Standard Schema-compatible library.

Related packages

License

MIT