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

v0.1.2

Published

Next.js server-side handlers for contract-kit

Readme

@contract-kit/next

Next.js adapter for the framework-agnostic @contract-kit/server runtime. It translates Next's Request/Response to/from the runtime's HttpRequestLike/HttpResponseLike shapes and exposes a minimal API for catch-all or per-contract routing.

Installation

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

Peer Dependencies

  • next: ^14.0.0 || ^15.0.0 || ^16.0.0
  • @contract-kit/openapi: ^0.1.0 (optional, for OpenAPI documentation)

TypeScript Requirements

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

Quick Start

1. Define Your Contracts

// app/contracts/todo.ts
import { createContractGroup } from "@contract-kit/core";
import { z } from "zod";

const todos = createContractGroup();

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

2. Create Your Server

// app/lib/server.ts
import { createNextServer } from "@contract-kit/next";
import { getTodo } from "../contracts/todo";

export const server = await createNextServer({
  ports: {},
  routes: [
    {
      contract: getTodo,
      handle: async ({ path }) => ({
        status: 200,
        body: {
          id: path.id,
          title: "Example todo",
          completed: false,
        },
      }),
    },
  ],
  createContext: async ({ req }) => {
    // Create request context (e.g., auth, database)
    return {
      userId: req.headers.get("x-user-id") || "anonymous",
    };
  },
  onUnhandledError: (error) => ({
    status: 500,
    body: { message: "Internal server error" },
  }),
});

3. Set Up Routes

You have two options for routing:

Option A: Catch-All Route (Recommended)

Create a catch-all route that handles all contracts:

// app/api/[...contract-kit]/route.ts
import { server } from "@/lib/server";

export const GET = server.api();
export const POST = server.api();
export const PUT = server.api();
export const PATCH = server.api();
export const DELETE = server.api();

Option B: Per-Contract Routes

Create individual route files for each contract:

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

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

API Reference

createNextServer<Ctx>(options)

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

Parameters:

  • options: Same as createServer from @contract-kit/server:
    • ports: Required - Ports object defining available service interfaces
    • createContext: Async function to create request context
    • onUnhandledError: Error handler function
    • routes?: Array of route configurations (contract + handler)
    • middleware?: Optional array of middleware
    • 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()

Returns a Next.js handler for catch-all routes. Use this with app/api/[...contract-kit]/route.ts to handle all registered contracts.

// app/api/[...contract-kit]/route.ts
import { server } from "@/lib/server";

export const GET = server.api();
export const POST = server.api();

server.handle(contract)

Returns a Next.js handler for a specific contract. Use this when you want to handle a contract without registering it globally.

// app/api/todos/route.ts
import { server } from "@/lib/server";
import { getTodo } from "@/contracts/todo";

export const GET = server.handle(getTodo);

server.route(contract)

Returns a route builder for creating custom handlers for a specific contract. The contract is NOT registered globally (won't be available via server.api()).

Returns: Route builder with:

  • handle(fn): Create a custom handler function
  • useCase(useCase, maps): Connect a use case with input/output mapping
// app/api/todos/[id]/route.ts
import { server } from "@/lib/server";
import { getTodo } from "@/contracts/todo";
import { getTodoUseCase } from "@/use-cases/get-todo";

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

// Option 2: Use case with mapping
export const GET = server
  .route(getTodo)
  .useCase(getTodoUseCase, {
    mapInput: ({ path }) => ({ id: path.id }),
    mapOutput: (result) => result,
  });

server.register(contract)

Returns a route builder (same as route()) but also registers the contract globally, making it available via server.api().

// app/lib/register-routes.ts
import { server } from "./server";
import { getTodo } from "../contracts/todo";

// Register and export handler
export const getTodoHandler = server
  .register(getTodo)
  .handle(async ({ ctx, path }) => {
    // Now this contract is also available via server.api()
    return { status: 200, body: { id: path.id, title: "..." } };
  });

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 "@/lib/server";
import { getTodoUseCase } from "@/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://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:

{
  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 object)
}

Use Case Integration

Contract Kit promotes clean architecture by separating use cases from HTTP concerns. Use the .useCase() method to connect use cases:

// 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)
  .useCase(getTodoUseCase, {
    mapInput: ({ path }) => ({ id: path.id }),
    mapOutput: (result) => result,
  });

Middleware

Middleware can be added at the server level:

import { createNextServer } from "@contract-kit/next";
import { errorMiddleware, loggingMiddleware } from "@contract-kit/server/middleware";

export const server = await createNextServer({
  ports: {},
  middleware: [
    loggingMiddleware({ logger: console }),
    errorMiddleware(),
  ],
  createContext: async () => ({}),
  onUnhandledError: () => ({ status: 500, body: { message: "Error" } }),
});

OpenAPI Documentation

If you have @contract-kit/openapi installed, you can generate OpenAPI documentation:

// app/api/openapi/route.ts
import { generateOpenApiSpec } from "@contract-kit/openapi";
import { getTodo } from "@/contracts/todo";

export async function GET() {
  const spec = generateOpenApiSpec({
    title: "My API",
    version: "1.0.0",
    contracts: [getTodo],
  });

  return Response.json(spec);
}

Providers

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

import { createNextServer } from "@contract-kit/next";
import { drizzleTursoProvider } from "@contract-kit/provider-drizzle-turso";
import { pinoLoggerProvider } from "@contract-kit/provider-logger-pino";

export const server = await createNextServer({
  ports: {},
  providers: [
    drizzleTursoProvider,
    pinoLoggerProvider,
  ],
  providerEnv: process.env,
  createContext: async ({ ports }) => ({
    // Access providers via ports
    db: ports.db,
    logger: ports.logger,
  }),
  onUnhandledError: () => ({ status: 500, body: { message: "Error" } }),
});

Error Handling

Global Error Handler

export const server = await createNextServer({
  ports: {},
  createContext: async () => ({}),
  onUnhandledError: (error, { ctx }) => {
    console.error("Unhandled error:", error);
    return {
      status: 500,
      body: {
        message: "Internal server error",
        ...(process.env.NODE_ENV === "development" && {
          error: error.message
        }),
      },
    };
  },
});

Route-Level Error Handling

export const GET = server
  .route(getTodo)
  .handle(async ({ ctx, path }) => {
    try {
      const todo = await fetchTodoById(path.id);
      return { status: 200, body: todo };
    } catch (error) {
      if (error.code === "NOT_FOUND") {
        return { status: 404, body: { message: "Todo not found" } };
      }
      throw error; // Will be caught by global error handler
    }
  });

Helper Functions

toRequestLike(req: Request): HttpRequestLike

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

toNextResponse(res: HttpResponseLike): Response

Converts an HttpResponseLike to a Next.js Response.

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

Examples

Basic CRUD API

// contracts/todos.ts
import { createContractGroup } from "@contract-kit/core";
import { z } from "zod";

const todos = createContractGroup();

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

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

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

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

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

export const deleteTodo = todos
  .delete("/todos/:id")
  .path(z.object({ id: z.string() }))
  .response(204, z.void());

// app/api/[...contract-kit]/route.ts
import { createNextServer } from "@contract-kit/next";
import * as todosContracts from "@/contracts/todos";

const server = await createNextServer({
  ports: {},
  routes: [
    { 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: [] }),
  onUnhandledError: () => ({ status: 500, body: { message: "Error" } }),
});

export const GET = server.api();
export const POST = server.api();
export const PUT = server.api();
export const DELETE = server.api();

With Authentication

// app/lib/server.ts
import { createNextServer } from "@contract-kit/next";
import { getTodo } from "@/contracts/todos";

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

    if (!user) {
      throw new Error("Unauthorized");
    }

    return { user };
  },
  onUnhandledError: (error) => {
    if (error.message === "Unauthorized") {
      return {
        status: 401,
        body: { message: "Unauthorized" }
      };
    }
    return {
      status: 500,
      body: { message: "Internal error" }
    };
  },
});

Server Component Usage

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

// app/todos/page.tsx
import { server } from "@/lib/server";
import { listTodosUseCase } from "@/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.todos.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