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

@contract-kit/server

v0.1.1

Published

Framework-agnostic HTTP server runtime for contract-kit - extracted from @contract-kit/app

Readme

@contract-kit/server

Framework-agnostic server runtime that wires HTTP contracts to handlers or use cases. The runtime is intentionally light so adapters (like @contract-kit/next) can translate requests and responses without re-implementing routing or middleware.

This package provides the core server functionality that powers framework-specific adapters. If you're using Next.js, see @contract-kit/next instead.

Installation

npm install @contract-kit/server @contract-kit/core

Peer Dependencies

  • @contract-kit/openapi: ^0.1.0 (optional, for OpenAPI spec generation)

TypeScript Requirements

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

Core Concepts

The server runtime is built around several key concepts:

  1. Contracts: Type-safe API definitions created with @contract-kit/core
  2. Handlers: Functions that implement contract endpoints
  3. Use Cases: Business logic functions that can be connected to contracts
  4. Middleware: Request/response interceptors for cross-cutting concerns
  5. Providers: Service implementations (database, cache, logger, etc.)
  6. Context: Per-request data available to all handlers

Quick Start

import { createServer } from "@contract-kit/server";
import { createContractGroup } from "@contract-kit/core";
import { z } from "zod";

// Define contracts
const todos = createContractGroup();

const getTodo = todos
  .get("/todos/:id")
  .path(z.object({ id: z.string() }))
  .response(200, z.object({
    id: z.string(),
    title: z.string(),
    completed: z.boolean(),
  }));

// Create server
const server = await createServer({
  ports: {}, // Required - your ports/services
  routes: [
    {
      contract: getTodo,
      handle: async ({ path }) => ({
        status: 200,
        body: {
          id: path.id,
          title: "Example todo",
          completed: false,
        },
      }),
    },
  ],
  createContext: async ({ req }) => ({
    requestId: req.headers.get("x-request-id") || crypto.randomUUID(),
  }),
  onUnhandledError: (error) => ({
    status: 500,
    body: { message: "Internal server error" },
  }),
});

// Use with any HTTP server
const request: HttpRequestLike = {
  method: "GET",
  url: "/todos/123",
  headers: new Headers(),
  json: async () => ({}),
  text: async () => "",
};

const response = await server.api()(request);
// { status: 200, body: { id: "123", title: "Example todo", completed: false } }

API Reference

createServer<Ctx, Ports>(options)

Creates a server instance with the given options.

Type Parameters:

  • Ctx: Type of the request context
  • Ports: Type of available ports (services)

Options:

  • ports: Required - Ports object defining available service interfaces
  • createContext: Function to create request context
  • onUnhandledError: Global error handler
  • routes?: Array of route configurations (contract + handler)
  • middleware?: Array of middleware functions
  • providers?: Array of service providers
  • providerEnv?: Environment variables for providers
  • providerConfig?: Configuration overrides for providers

Returns: Promise<ServerInstance<Ctx>>

ServerInstance Methods

server.api()

Returns a handler function that routes requests based on method and path.

const handler = server.api();
const response = await handler(request);

This is typically used with catch-all routing in framework adapters.

server.handle(contract)

Creates a handler for a specific contract without registering it globally.

const handler = server.handle(getTodo);
const response = await handler(request);

server.route(contract)

Returns a route builder for creating handlers. Handlers created this way are NOT registered globally.

Returns: Route builder with:

  • handle(fn): Create a custom handler
  • useCase(useCase, maps): Connect a use case
const handler = server.route(getTodo).handle(async ({ ctx, path }) => {
  // Implementation
  return { status: 200, body: { id: path.id, title: "..." } };
});

server.register(contract)

Returns a route builder (same as route()) but registers handlers globally for use with server.api().

const handler = server.register(getTodo).handle(async ({ ctx, path }) => {
  // This handler is now available via server.api()
  return { status: 200, body: { id: path.id, title: "..." } };
});

server.stop()

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

await server.stop();

Route Configuration

Inline Route Configuration

Pass route configurations directly to createServer:

// Define contracts
const todos = createContractGroup();

const getTodo = todos
  .get("/todos/:id")
  .path(z.object({ id: z.string() }))
  .response(200, TodoSchema);

const createTodo = todos
  .post("/todos")
  .body(z.object({ title: z.string() }))
  .response(201, TodoSchema);

const server = await createServer({
  ports: {},
  routes: [
    {
      contract: getTodo,
      handle: async ({ ctx, path }) => {
        const todo = await ctx.db.todos.findById(path.id);
        return { status: 200, body: todo };
      },
    },
    {
      contract: createTodo,
      useCase: createTodoUseCase,
      mapInput: ({ body }) => body,
      mapOutput: (result) => result,
    },
  ],
  createContext: async ({ ports }) => ({ db: ports.db }),
  onUnhandledError: () => ({ status: 500, body: { message: "Error" } }),
});

Using Route Builders

Alternatively, use register() for more flexibility:

const server = await createServer({
  ports: {},
  createContext: async ({ ports }) => ({ db: ports.db }),
  onUnhandledError: () => ({ status: 500, body: { message: "Error" } }),
});

// Register routes
server.register(getTodo).handle(async ({ ctx, path }) => {
  const todo = await ctx.db.todos.findById(path.id);
  return { status: 200, body: todo };
});

server.register(createTodo).useCase(createTodoUseCase, {
  mapInput: ({ body }) => body,
  mapOutput: (result) => result,
});

Context Creation

The createContext function runs for every request and provides data to handlers:

const server = await createServer({
  ports: {},
  createContext: async ({ req, ports }) => {
    // Access request
    const token = req.headers.get("authorization");

    // Access ports (from providers)
    const user = token ? await ports.db.users.findByToken(token) : null;

    // Return context
    return {
      user,
      requestId: req.headers.get("x-request-id"),
    };
  },
  onUnhandledError: () => ({ status: 500, body: { message: "Error" } }),
});

The context is then available in all handlers:

server.register(getTodo).handle(async ({ ctx, path }) => {
  // ctx.user and ctx.requestId are available
  if (!ctx.user) {
    return { status: 401, body: { message: "Unauthorized" } };
  }
  // ...
});

Use Cases

Use cases are pure business logic functions that are separate from HTTP concerns:

// use-cases/create-todo.ts
export async function createTodoUseCase(
  input: { title: string },
  ports: { db: DbPort }
) {
  return await ports.db.todos.create({
    id: crypto.randomUUID(),
    title: input.title,
    completed: false,
  });
}

// server.ts
server.register(createTodo).useCase(createTodoUseCase, {
  mapInput: ({ body }) => ({ title: body.title }),
  mapOutput: (todo) => todo,
});

Benefits:

  • Testable without HTTP infrastructure
  • Reusable across different HTTP endpoints
  • Clear separation of business logic and HTTP concerns

Middleware

Middleware intercepts requests and responses for cross-cutting concerns:

import { createServer } from "@contract-kit/server";
import {
  loggingMiddleware,
  errorMiddleware,
  corsMiddleware,
  rateLimitMiddleware
} from "@contract-kit/server/middleware";

const server = await createServer({
  ports: {},
  middleware: [
    loggingMiddleware({
      logger: console,
      logBody: false
    }),
    corsMiddleware({
      origin: "*",
      credentials: true,
    }),
    rateLimitMiddleware({
      windowMs: 60000,
      max: 100,
    }),
    errorMiddleware(),
  ],
  createContext: async () => ({}),
  onUnhandledError: () => ({ status: 500, body: { message: "Error" } }),
});

Built-in Middleware

The @contract-kit/server/middleware export provides:

  • loggingMiddleware: Request/response logging
  • errorMiddleware: Error handling and transformation
  • corsMiddleware: CORS headers
  • rateLimitMiddleware: Rate limiting (requires rate limit port)

Custom Middleware

Create your own middleware:

import type { Middleware } from "@contract-kit/server";

const authMiddleware: Middleware<MyContext> = async (req, next) => {
  const token = req.headers.get("authorization");

  if (!token) {
    return {
      status: 401,
      body: { message: "Missing authorization" },
    };
  }

  // Continue to next middleware/handler
  return await next(req);
};

const server = await createServer({
  ports: {},
  middleware: [authMiddleware],
  createContext: async () => ({}),
  onUnhandledError: () => ({ status: 500, body: { message: "Error" } }),
});

Providers

Providers are adapters that implement ports (service interfaces):

import { createServer } from "@contract-kit/server";
import { drizzleTursoProvider } from "@contract-kit/provider-drizzle-turso";
import { pinoLoggerProvider } from "@contract-kit/provider-logger-pino";
import { redisProvider } from "@contract-kit/provider-redis";

const server = await createServer({
  ports: {},
  providers: [
    drizzleTursoProvider,
    pinoLoggerProvider,
    redisProvider,
  ],
  providerEnv: process.env,
  providerConfig: {
    // Override provider defaults
    logger: {
      level: "debug",
    },
  },
  createContext: async ({ ports }) => ({
    db: ports.db,
    logger: ports.logger,
    cache: ports.cache,
  }),
  onUnhandledError: () => ({ status: 500, body: { message: "Error" } }),
});

Available Providers

  • @contract-kit/provider-drizzle-turso: Database (Drizzle ORM + Turso)
  • @contract-kit/provider-logger-pino: Logging (Pino)
  • @contract-kit/provider-redis: Caching (Redis via ioredis)
  • @contract-kit/provider-rate-limit-upstash: Rate limiting (Upstash)
  • @contract-kit/provider-inngest: Background jobs (Inngest)
  • @contract-kit/provider-auth-better-auth: Authentication (Better Auth)
  • @contract-kit/provider-mail-resend: Email (Resend)
  • @contract-kit/provider-mail-smtp: Email (SMTP)
  • @contract-kit/provider-event-bus-memory: Event bus (in-memory)

Error Handling

Global Error Handler

const server = await createServer({
  ports: {},
  createContext: async () => ({}),
  onUnhandledError: (error, context) => {
    // Log error
    console.error("Unhandled error:", error);

    // Access context
    console.error("Request:", context.req.method, context.req.url);

    // Return error response
    return {
      status: 500,
      body: {
        message: "Internal server error",
        ...(process.env.NODE_ENV === "development" && {
          error: error.message,
          stack: error.stack,
        }),
      },
    };
  },
});

Route-Level Error Handling

server.register(getTodo).handle(async ({ ctx, path }) => {
  try {
    const todo = await ctx.db.todos.findById(path.id);
    return { status: 200, body: todo };
  } catch (error) {
    if (error.code === "NOT_FOUND") {
      return { status: 404, body: { message: "Todo not found" } };
    }
    if (error.code === "FORBIDDEN") {
      return { status: 403, body: { message: "Access denied" } };
    }
    // Re-throw to be caught by global error handler
    throw error;
  }
});

Using Error Middleware

import { errorMiddleware } from "@contract-kit/server/middleware";
import { HttpError } from "@contract-kit/errors/http";

const server = await createServer({
  ports: {},
  middleware: [
    errorMiddleware({
      includeStack: process.env.NODE_ENV === "development",
    }),
  ],
  createContext: async () => ({}),
  onUnhandledError: () => ({ status: 500, body: { message: "Error" } }),
});

// Throw HTTP errors in handlers
server.register(getTodo).handle(async ({ ctx, path }) => {
  const todo = await ctx.db.todos.findById(path.id);

  if (!todo) {
    throw new HttpError(404, "Todo not found");
  }

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

Framework-Agnostic Types

The server uses framework-agnostic request/response types:

HttpRequestLike

interface HttpRequestLike {
  method: string;           // "GET", "POST", etc.
  url: string;              // Full URL or path
  headers: Headers;         // Web API Headers object
  json(): Promise<unknown>; // Parse JSON body
  text(): Promise<string>;  // Get raw body
}

HttpResponseLike

interface HttpResponseLike {
  status: number;                      // HTTP status code
  headers?: Record<string, string>;    // Response headers
  body?: unknown;                      // Response body (serialized to JSON)
}

These types allow the server runtime to work with any HTTP server or framework.

Handler Function Context

Handler functions receive a context object with:

{
  ctx: Ctx,              // Your custom context from createContext
  path: PathParams,      // Validated path parameters
  query: QueryParams,    // Validated query parameters
  body: Body,            // Validated request body
  headers: Headers,      // Request headers (Web API Headers)
  req: HttpRequestLike,  // Raw request object
  ports: Ports,          // Available ports from providers
  contract: Contract,    // The contract being handled
}

Examples

REST API with Database

import { createServer } from "@contract-kit/server";
import { createContractGroup } from "@contract-kit/core";
import { drizzleTursoProvider } from "@contract-kit/provider-drizzle-turso";
import { z } from "zod";

// Define contracts
const todos = createContractGroup();

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

const listTodos = todos
  .get("/todos")
  .response(200, z.array(TodoSchema));

const getTodo = todos
  .get("/todos/:id")
  .path(z.object({ id: z.string() }))
  .response(200, TodoSchema);

const createTodo = todos
  .post("/todos")
  .body(z.object({ title: z.string() }))
  .response(201, TodoSchema);

const server = await createServer({
  ports: {},
  providers: [drizzleTursoProvider],
  providerEnv: process.env,
  routes: [
    {
      contract: listTodos,
      handle: async ({ ports }) => {
        const todos = await ports.db.todos.findAll();
        return { status: 200, body: todos };
      },
    },
    {
      contract: getTodo,
      handle: async ({ ports, path }) => {
        const todo = await ports.db.todos.findById(path.id);
        if (!todo) {
          return { status: 404, body: { message: "Not found" } };
        }
        return { status: 200, body: todo };
      },
    },
    {
      contract: createTodo,
      handle: async ({ ports, body }) => {
        const todo = await ports.db.todos.create(body);
        return { status: 201, body: todo };
      },
    },
  ],
  createContext: async ({ ports }) => ({ db: ports.db }),
  onUnhandledError: () => ({ status: 500, body: { message: "Error" } }),
});

With Authentication

const server = await createServer({
  ports: {},
  createContext: async ({ req, ports }) => {
    const token = req.headers.get("authorization")?.replace("Bearer ", "");

    if (!token) {
      return { user: null };
    }

    try {
      const user = await ports.auth.validateToken(token);
      return { user };
    } catch {
      return { user: null };
    }
  },
  onUnhandledError: () => ({ status: 500, body: { message: "Error" } }),
});

const protectedRoute = createContractGroup()
  .get("/me")
  .response(200, z.object({ userId: z.string() }));

server.register(protectedRoute).handle(async ({ ctx }) => {
  if (!ctx.user) {
    return { status: 401, body: { message: "Unauthorized" } };
  }

  // User is authenticated
  return { status: 200, body: { userId: ctx.user.id } };
});

With Background Jobs

import { createServer } from "@contract-kit/server";
import { createContractGroup } from "@contract-kit/core";
import { inngestProvider } from "@contract-kit/provider-inngest";
import { z } from "zod";

const api = createContractGroup();

const sendEmail = api
  .post("/send-email")
  .body(z.object({ email: z.string().email(), subject: z.string() }))
  .response(202, z.object({ message: z.string() }));

const server = await createServer({
  ports: {},
  providers: [inngestProvider],
  providerEnv: process.env,
  routes: [
    {
      contract: sendEmail,
      handle: async ({ ports, body }) => {
        // Queue background job
        await ports.jobs.send({
          name: "send-email",
          data: { to: body.email, subject: body.subject },
        });

        return { status: 202, body: { message: "Email queued" } };
      },
    },
  ],
  createContext: async ({ ports }) => ({ jobs: ports.jobs }),
  onUnhandledError: () => ({ status: 500, body: { message: "Error" } }),
});

Integration with Framework Adapters

This package provides the core runtime. For framework-specific integrations:

Next.js

Use @contract-kit/next:

import { createNextServer } from "@contract-kit/next";

const server = await createNextServer({
  ports: {},
  createContext: async ({ req }) => ({ /* ... */ }),
  onUnhandledError: () => ({ status: 500, body: { message: "Error" } }),
});

// app/api/[...contract-kit]/route.ts
export const GET = server.api();
export const POST = server.api();

Other Frameworks

Create your own adapter by translating framework requests/responses to HttpRequestLike/HttpResponseLike:

import { createServer } from "@contract-kit/server";

const runtime = await createServer({ /* ... */ });
const handler = runtime.api();

// Express example
app.all("*", async (req, res) => {
  const request: HttpRequestLike = {
    method: req.method,
    url: req.url,
    headers: new Headers(req.headers as Record<string, string>),
    json: async () => req.body,
    text: async () => JSON.stringify(req.body),
  };

  const response = await handler(request);

  if (response.headers) {
    Object.entries(response.headers).forEach(([key, value]) => {
      res.setHeader(key, value);
    });
  }

  res.status(response.status).json(response.body);
});

Testing

The framework-agnostic design makes testing easy:

import { createServer } from "@contract-kit/server";
import { createContractGroup } from "@contract-kit/core";
import { z } from "zod";
import { expect, test } from "bun:test";

test("get todo returns 200", async () => {
  const todos = createContractGroup();
  const getTodo = todos
    .get("/todos/:id")
    .path(z.object({ id: z.string() }))
    .response(200, z.object({
      id: z.string(),
      title: z.string(),
      completed: z.boolean(),
    }));

  const server = await createServer({
    ports: {},
    routes: [
      {
        contract: getTodo,
        handle: async ({ path }) => ({
          status: 200,
          body: { id: path.id, title: "Test", completed: false },
        }),
      },
    ],
    createContext: async () => ({}),
    onUnhandledError: () => ({ status: 500, body: { message: "Error" } }),
  });

  const handler = server.api();

  const response = await handler({
    method: "GET",
    url: "/todos/123",
    headers: new Headers(),
    json: async () => ({}),
    text: async () => "",
  });

  expect(response.status).toBe(200);
  expect(response.body).toEqual({
    id: "123",
    title: "Test",
    completed: false,
  });
});

Related Packages

License

MIT