Skip to main content

What are Providers?

Providers are concrete implementations of ports that connect your application to external services. They follow the hexagonal architecture pattern, making your code more testable and maintainable.

Available Providers

Database

Drizzle + Turso

SQLite database with Drizzle ORM and Turso hosting

Caching

Redis

In-memory caching with Redis

Email

Authentication

Better Auth

Full-featured authentication solution

Background Jobs

Inngest

Reliable background job processing

Rate Limiting

Upstash Rate Limit

Distributed rate limiting

Using Providers

1. Install the Provider

npm install @contract-kit/provider-redis ioredis

2. Configure the Provider

// lib/providers/redis.ts
import { createRedisProvider } from "@contract-kit/provider-redis";
import Redis from "ioredis";

const redis = new Redis(process.env.REDIS_URL);

export const redisProvider = createRedisProvider({ client: redis });

3. Register with Ports

// lib/ports.ts
import { definePorts } from "contract-kit";
import { redisProvider } from "./providers/redis";

export const ports = definePorts({
  cache: redisProvider,
});

4. Use in Your Application

export const GET = server.route(getTodo).handle(async ({ ctx, path }) => {
  // Try cache first
  const cached = await ctx.ports.cache.get(`todo:${path.id}`);
  if (cached) {
    return { status: 200, body: JSON.parse(cached) };
  }

  // Fetch from database
  const todo = await ctx.ports.db.todos.findById(path.id);

  // Cache for next time
  await ctx.ports.cache.set(`todo:${path.id}`, JSON.stringify(todo), 300);

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

Creating Custom Providers

You can create custom providers for services not yet supported:
// providers/my-custom-provider.ts
import { createProvider } from "contract-kit";

export interface MyServicePort {
  doSomething: (input: string) => Promise<string>;
}

export const myServiceProvider = createProvider({
  name: "my-service",
  async register({ ports }) {
    ports.extend("myService", {
      doSomething: async (input: string) => {
        // Implementation
        return `Processed: ${input}`;
      },
    });
  },
});

Provider Patterns

Environment Configuration

export const createProviders = () => {
  return {
    db: createDrizzleProvider({
      url: process.env.DATABASE_URL!,
    }),
    cache: createRedisProvider({
      url: process.env.REDIS_URL!,
    }),
    mail: createResendProvider({
      apiKey: process.env.RESEND_API_KEY!,
    }),
  };
};

Lazy Initialization

let redisClient: Redis | null = null;

export const getRedisProvider = () => {
  if (!redisClient) {
    redisClient = new Redis(process.env.REDIS_URL);
  }
  return createRedisProvider({ client: redisClient });
};

Testing with Mock Providers

import { describe, it, expect } from "bun:test";

describe("getTodo", () => {
  it("should fetch from cache", async () => {
    const mockCache = {
      get: async (key: string) => '{"id":"123","title":"Test"}',
      set: async () => {},
    };

    const mockPorts = {
      cache: mockCache,
      db: { todos: { findById: async () => null } },
    };

    // Test with mock
  });
});

Best Practices

Store API keys and connection strings in environment variables, never in code.
Create provider instances once at startup and reuse them across requests.
Implement proper error handling and reconnection logic for external services.
Clean up connections on shutdown to prevent resource leaks.
Create mock providers for unit tests to avoid external dependencies.

Common Provider Combinations

Basic Web App

const ports = definePorts({
  db: drizzleProvider,
  cache: redisProvider,
  mail: resendProvider,
});

API with Auth

const ports = definePorts({
  db: drizzleProvider,
  auth: betterAuthProvider,
  cache: redisProvider,
  rateLimit: upstashRateLimitProvider,
});
const ports = definePorts({
  db: drizzleProvider,
  auth: betterAuthProvider,
  cache: redisProvider,
  mail: resendProvider,
  jobs: inngestProvider,
  rateLimit: upstashRateLimitProvider,
  logger: pinoProvider,
});

Next Steps