🚧

Alpha Release - This project is in early development and APIs may change

ts-contractts-contract
API ReferenceCore API

initContract

API reference for the initContract function and ContractBuilder.

Overview

initContract creates a builder that allows you to compose plugins onto your contract. It returns a ContractBuilder instance with .use() and .build() methods.

Import

import { initContract } from '@ts-contract/core';

Signature

function initContract<C extends ContractDef>(
  contract: C
): ContractBuilder<C, []>

Type Parameters

  • C - The contract definition type, inferred from the argument

Parameters

  • contract - A contract created with createContract()

Returns

A ContractBuilder instance with no plugins initially applied

ContractBuilder API

The builder returned by initContract has two methods:

.use()

Adds a plugin to the builder.

use<P extends ContractPlugin>(plugin: P): ContractBuilder<C, [...Ps, P]>

Parameters:

  • plugin - A plugin object implementing ContractPlugin

Returns:

  • A new builder with the plugin added

Example:

import { pathPlugin, validatePlugin } from '@ts-contract/plugins';

const builder = initContract(contract)
  .use(pathPlugin)
  .use(validatePlugin);

.build()

Produces the final enhanced contract with all plugin methods.

build(): ApplyPlugins<C, Ps>

Returns:

  • The contract with all plugin methods added to each route

Example:

const api = initContract(contract)
  .use(pathPlugin)
  .use(validatePlugin)
  .build();

// Now routes have plugin methods
api.getUser.buildPath({ id: '123' });
api.getUser.validateResponse(200, data);

Usage

Basic Usage

import { createContract, initContract } from '@ts-contract/core';
import { pathPlugin, validatePlugin } from '@ts-contract/plugins';
import { z } from 'zod';

const contract = createContract({
  getUser: {
    method: 'GET',
    path: '/users/:id',
    pathParams: z.object({ id: z.string() }),
    responses: {
      200: z.object({ id: z.string(), name: z.string() }),
    },
  },
});

const api = initContract(contract)
  .use(pathPlugin)
  .use(validatePlugin)
  .build();

Conditional Plugins

Add plugins conditionally based on environment:

const builder = initContract(contract).use(pathPlugin);

const api = process.env.NODE_ENV === 'development'
  ? builder.use(validatePlugin).build()
  : builder.build();

Custom Plugin Chain

import { customLoggerPlugin } from './plugins/logger';
import { customCachePlugin } from './plugins/cache';
import { customMetricsPlugin } from './plugins/metrics';

const api = initContract(contract)
  .use(pathPlugin)
  .use(validatePlugin)
  .use(customLoggerPlugin)
  .use(customCachePlugin)
  .use(customMetricsPlugin)
  .build();

ContractPlugin

interface ContractPlugin<Name extends string = string> {
  name: Name;
  route: (route: RouteDef) => Record<string, unknown>;
}

A plugin object with:

  • name - Unique identifier for the plugin
  • route - Function that receives a route and returns methods to add

ApplyPlugins

type ApplyPlugins<C, Ps extends readonly ContractPlugin[]> = {
  [K in keyof C]: C[K] extends RouteDef
    ? MergePluginReturns<C[K], Ps>
    : C[K] extends ContractDef
      ? ApplyPlugins<C[K], Ps>
      : never;
};

Type that recursively applies plugin methods to all routes in a contract.

PluginTypeRegistry

interface PluginTypeRegistry<R> {
  // Plugins register their types here via declaration merging
}

Type registry that plugins use to declare their return types.

Performance

initContract and the builder have minimal overhead:

  • Build time: Negligible - simple object mapping
  • Runtime: No ongoing cost after .build()
  • Memory: Small - one object per route with plugin methods
  • Bundle size: ~1KB for the builder logic

The performance impact comes from the plugins themselves, not from initContract.

Common Patterns

Shared API Instance

Create one API instance and share it:

// api.ts
export const api = initContract(contract)
  .use(pathPlugin)
  .use(validatePlugin)
  .build();

// Multiple files can import and use it
import { api } from './api';

Separate Client and Server APIs

Create different API instances for client and server:

// client-api.ts
export const clientApi = initContract(contract)
  .use(pathPlugin)
  .use(validatePlugin)
  .build();

// server-api.ts
export const serverApi = initContract(contract)
  .use(validatePlugin)
  .build();

Plugin Factory

Create a factory for consistent plugin configuration:

function createApi<C extends ContractDef>(contract: C) {
  return initContract(contract)
    .use(pathPlugin)
    .use(validatePlugin)
    .build();
}

const userApi = createApi(userContract);
const postApi = createApi(postContract);

Troubleshooting

Plugin Methods Not Available

Problem: Plugin methods don't exist on routes.

Solution: Make sure you called .build():

// ✗ Wrong - forgot .build()
const api = initContract(contract).use(pathPlugin);
api.getUser.buildPath({ id: '123' }); // Error!

// ✓ Correct
const api = initContract(contract).use(pathPlugin).build();
api.getUser.buildPath({ id: '123' }); // Works!

Type Errors with Plugins

Problem: TypeScript shows errors with plugin methods.

Solution: Ensure plugins are properly typed with declaration merging:

// Plugin must declare its types
declare module '@ts-contract/core' {
  interface PluginTypeRegistry<R> {
    myPlugin: {
      myMethod: () => string;
    };
  }
}

Plugin Order Issues

Problem: Plugin methods conflict or override each other.

Solution: Check plugin order and ensure plugins don't add methods with the same name:

// If plugins conflict, order matters
const api = initContract(contract)
  .use(pluginA) // Adds method 'foo'
  .use(pluginB) // Also adds method 'foo' - will override pluginA's version
  .build();

See Also