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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@contract-kit/ports

v0.1.1

Published

Framework-agnostic port definitions for contract-kit - standardize outbound dependencies like db, mailer, cache

Readme

@contract-kit/ports

Ports & Adapters pattern for Contract Kit

This package provides a simple way to define and type your application's outbound dependencies (database, mailer, cache, event bus, etc.) following the hexagonal architecture pattern.

Installation

npm install @contract-kit/ports

TypeScript Requirements

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

Concepts

Ports

A port is an interface that your application uses to interact with the outside world. Examples include:

  • Database access
  • Email sending
  • Caching
  • Event publishing
  • External API calls

Adapters

An adapter is an implementation of a port. You can swap adapters without changing your application code:

  • Production: Real database adapter
  • Testing: In-memory mock adapter
  • Development: Local file-based adapter

Usage

Defining Ports

// lib/ports.ts
import { definePorts } from "@contract-kit/ports";

// Database adapter interface
interface DbAdapter {
  todos: {
    findById(id: string): Promise<Todo | null>;
    create(data: CreateTodoData): Promise<Todo>;
    update(id: string, data: UpdateTodoData): Promise<Todo | null>;
    delete(id: string): Promise<boolean>;
    list(filter?: TodoFilter): Promise<Todo[]>;
  };
  users: {
    findById(id: string): Promise<User | null>;
    findByEmail(email: string): Promise<User | null>;
  };
}

// Email adapter interface
interface MailerAdapter {
  send(options: {
    to: string;
    subject: string;
    body: string;
  }): Promise<void>;
}

// Cache adapter interface
interface CacheAdapter {
  get<T>(key: string): Promise<T | null>;
  set<T>(key: string, value: T, ttlSeconds?: number): Promise<void>;
  delete(key: string): Promise<void>;
}

// Event bus adapter interface
interface EventBusAdapter {
  publish(event: DomainEvent): Promise<void>;
  subscribe(type: string, handler: (event: DomainEvent) => void): () => void;
}

// Define your ports
export const ports = definePorts({
  db: dbAdapter,
  mailer: mailerAdapter,
  cache: cacheAdapter,
  eventBus: eventBusAdapter,
});

export type AppPorts = typeof ports;

Application Context

Use PortsContext to type your application context:

// lib/ctx.ts
import type { PortsContext } from "@contract-kit/ports";
import type { AppPorts } from "./ports";

export interface AppCtx extends PortsContext<AppPorts> {
  user: { id: string; role: string } | null;
  requestId: string;
  now: () => Date;
}

Using Ports in Use Cases

// application/todos/create.ts
import { z } from "zod";
import { useCase } from "../use-case";

export const createTodo = useCase
  .command("todos.create")
  .input(z.object({ title: z.string() }))
  .output(z.object({ id: z.string(), title: z.string() }))
  .run(async ({ ctx, input }) => {
    // Access ports through ctx.ports
    const todo = await ctx.ports.db.todos.create({
      id: crypto.randomUUID(),
      title: input.title,
      completed: false,
    });

    // Use other ports
    await ctx.ports.cache.delete("todos:list");
    
    return { id: todo.id, title: todo.title };
  });

Creating Adapters

Production Adapters

// adapters/db/prisma.ts
import { prisma } from "@/lib/prisma";
import type { DbAdapter } from "@/lib/ports";

export const prismaDbAdapter: DbAdapter = {
  todos: {
    findById: (id) => prisma.todo.findUnique({ where: { id } }),
    create: (data) => prisma.todo.create({ data }),
    update: (id, data) => prisma.todo.update({ where: { id }, data }),
    delete: async (id) => {
      await prisma.todo.delete({ where: { id } });
      return true;
    },
    list: (filter) => prisma.todo.findMany({ where: filter }),
  },
  users: {
    findById: (id) => prisma.user.findUnique({ where: { id } }),
    findByEmail: (email) => prisma.user.findUnique({ where: { email } }),
  },
};

Test Adapters

// adapters/db/memory.ts
import type { DbAdapter } from "@/lib/ports";

export function createMemoryDbAdapter(): DbAdapter {
  const todos = new Map<string, Todo>();
  const users = new Map<string, User>();

  return {
    todos: {
      findById: async (id) => todos.get(id) ?? null,
      create: async (data) => {
        todos.set(data.id, data);
        return data;
      },
      update: async (id, data) => {
        const todo = todos.get(id);
        if (!todo) return null;
        const updated = { ...todo, ...data };
        todos.set(id, updated);
        return updated;
      },
      delete: async (id) => {
        return todos.delete(id);
      },
      list: async (filter) => {
        return Array.from(todos.values()).filter((t) =>
          !filter?.completed || t.completed === filter.completed
        );
      },
    },
    users: {
      findById: async (id) => users.get(id) ?? null,
      findByEmail: async (email) =>
        Array.from(users.values()).find((u) => u.email === email) ?? null,
    },
  };
}

Wiring Up in Next.js

// lib/server.ts
import { createNextRouter } from "@contract-kit/next";
import { prismaDbAdapter } from "@/adapters/db/prisma";
import { resendMailerAdapter } from "@/adapters/mailer/resend";
import { redisCache } from "@/adapters/cache/redis";

export const router = createNextRouter({
  createContext: async (req) => ({
    ports: {
      db: prismaDbAdapter,
      mailer: resendMailerAdapter,
      cache: redisCache,
      eventBus: eventBusAdapter,
    },
    user: await getUser(req),
    requestId: crypto.randomUUID(),
    now: () => new Date(),
  }),
});

Testing with Mock Ports

// __tests__/todos.test.ts
import { createTodo } from "@/application/todos/create";
import { createMemoryDbAdapter } from "@/adapters/db/memory";

describe("createTodo", () => {
  it("creates a todo", async () => {
    const db = createMemoryDbAdapter();
    const cache = {
      get: vi.fn(),
      set: vi.fn(),
      delete: vi.fn(),
    };

    const ctx = {
      ports: { db, cache, mailer: mockMailer, eventBus: mockEventBus },
      user: { id: "user-1", role: "user" },
      requestId: "req-1",
      now: () => new Date("2024-01-01"),
    };

    const result = await createTodo.run({
      ctx,
      input: { title: "Test Todo" },
    });

    expect(result.title).toBe("Test Todo");
    expect(cache.delete).toHaveBeenCalledWith("todos:list");
  });
});

API Reference

definePorts(ports)

A typed identity function that captures the shape of your ports object.

const ports = definePorts({
  db: dbAdapter,
  mailer: mailerAdapter,
});

// Export the type for use elsewhere
export type AppPorts = typeof ports;

PortsContext<P>

A type helper for context objects that carry ports.

interface AppCtx extends PortsContext<AppPorts> {
  // Additional context properties
  user: User | null;
}

// Equivalent to:
interface AppCtx {
  ports: AppPorts;
  user: User | null;
}

Benefits

  1. Dependency Inversion - Your application code depends on abstractions (ports), not implementations (adapters)

  2. Testability - Easily swap real adapters for test doubles

  3. Flexibility - Change implementations without touching business logic

  4. Type Safety - Full TypeScript support for all port interfaces

  5. Clean Architecture - Clear separation between application logic and infrastructure

Related Packages

License

MIT