🚧

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

ts-contractts-contract
API ReferencePlugins API

validatePlugin API

Complete API reference for the validatePlugin.

Overview

The validatePlugin is a built-in plugin that adds validation methods to each route for runtime schema validation.

Import

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

Plugin Object

const validatePlugin: ContractPlugin<'validate'>

Properties

  • name: 'validate'
  • route: Function that adds validation methods to routes

Usage

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

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

Added Methods

validatePathParams()

Validates path parameters against the route's pathParams schema.

Signature

validatePathParams(params: unknown): InferPathParams<Route>

Parameters

  • params - Unknown data to validate

Returns

  • Validated and typed path parameters

Throws

  • Error if validation fails or if route has no pathParams schema

Example

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

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

// ✓ Valid
const params = api.getUser.validatePathParams({
  id: '550e8400-e29b-41d4-a716-446655440000',
});
// => { id: '550e8400-e29b-41d4-a716-446655440000' }

// ✗ Throws validation error
api.getUser.validatePathParams({ id: 'not-a-uuid' });

validateQuery()

Validates query parameters against the route's query schema.

Signature

validateQuery(query: unknown): InferQuery<Route>

Parameters

  • query - Unknown data to validate

Returns

  • Validated and typed query parameters

Throws

  • Error if validation fails or if route has no query schema

Example

const contract = createContract({
  listUsers: {
    method: 'GET',
    path: '/users',
    query: z.object({
      page: z.string().transform(Number),
      limit: z.string().transform(Number).optional(),
    }),
    responses: {
      200: z.array(z.object({ id: z.string() })),
    },
  },
});

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

// ✓ Valid - transforms strings to numbers
const query = api.listUsers.validateQuery({ page: '2', limit: '10' });
// => { page: 2, limit: 10 }

// ✗ Throws validation error
api.listUsers.validateQuery({ page: 'invalid' });

validateBody()

Validates request body against the route's body schema.

Signature

validateBody(body: unknown): InferBody<Route>

Parameters

  • body - Unknown data to validate

Returns

  • Validated and typed request body

Throws

  • Error if validation fails or if route has no body schema

Example

const contract = createContract({
  createUser: {
    method: 'POST',
    path: '/users',
    body: z.object({
      name: z.string().min(1),
      email: z.string().email(),
    }),
    responses: {
      201: z.object({ id: z.string() }),
    },
  },
});

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

// ✓ Valid
const body = api.createUser.validateBody({
  name: 'Alice',
  email: '[email protected]',
});

// ✗ Throws validation error
api.createUser.validateBody({
  name: '',
  email: 'not-an-email',
});

validateResponse()

Validates response data against the route's response schema for a specific status code.

Signature

validateResponse<S extends keyof Route['responses'] & HttpStatusCodes>(
  status: S,
  data: unknown
): InferResponseBody<Route, S>

Type Parameters

  • S - HTTP status code (must be defined in route's responses)

Parameters

  • status - HTTP status code
  • data - Unknown data to validate

Returns

  • Validated and typed response body for the specified status code

Throws

  • Error if validation fails or if route has no response schema for the status code

Example

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(),
        email: z.string().email(),
      }),
      404: z.object({
        message: z.string(),
      }),
    },
  },
});

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

// ✓ Valid - 200 response
const user = api.getUser.validateResponse(200, {
  id: '123',
  name: 'Alice',
  email: '[email protected]',
});

// ✓ Valid - 404 response
const error = api.getUser.validateResponse(404, {
  message: 'User not found',
});

// ✗ Throws validation error - wrong schema for status
api.getUser.validateResponse(200, {
  message: 'User not found',
});

validateHeaders()

Validates request headers against the route's headers schema.

Signature

validateHeaders(headers: Record<string, unknown>): InferHeaders<Route>

Parameters

  • headers - Object with header names and values

Returns

  • Validated and typed headers

Throws

  • Error if validation fails or if route has no headers schema

Example

const contract = createContract({
  getProtected: {
    method: 'GET',
    path: '/protected',
    headers: {
      'authorization': z.string().startsWith('Bearer '),
      'x-api-key': z.string(),
    },
    responses: {
      200: z.object({ data: z.string() }),
    },
  },
});

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

// ✓ Valid
const headers = api.getProtected.validateHeaders({
  'authorization': 'Bearer token123',
  'x-api-key': 'key123',
});

// ✗ Throws validation error
api.getProtected.validateHeaders({
  'authorization': 'token123', // Missing "Bearer " prefix
  'x-api-key': 'key123',
});

Error Handling

All validation methods throw errors with descriptive messages:

try {
  api.createUser.validateBody({
    name: '',
    email: 'invalid-email',
  });
} catch (error) {
  console.error(error.message);
  // => "Validation failed for body of /users: String must contain at least 1 character(s), Invalid email"
}

Error messages include:

  • What failed (pathParams, query, body, response, headers)
  • Which route (path)
  • Specific validation issues from the schema library

Missing Schema Errors

If you try to validate a field that doesn't have a schema:

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

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

// Throws: Error: Route "/users/:id" has no body schema
api.getUser.validateBody({ name: 'Alice' });

Integration with Standard Schema

The validate plugin works with any schema library implementing @standard-schema/spec:

  • Zod - z.object(), z.string(), etc.
  • Valibot - v.object(), v.string(), etc.
  • Arktype - type() definitions
  • Any custom Standard Schema implementation

Type Safety

All validation methods return properly typed values:

const contract = createContract({
  createUser: {
    method: 'POST',
    path: '/users',
    body: z.object({
      name: z.string(),
      email: z.string().email(),
    }),
    responses: {
      201: z.object({
        id: z.string(),
        name: z.string(),
      }),
    },
  },
});

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

const body = api.createUser.validateBody(unknownData);
// body is typed as { name: string; email: string }

const response = api.createUser.validateResponse(201, unknownData);
// response is typed as { id: string; name: string }

Performance

  • Bundle size: ~800 bytes minified + gzipped (plus your schema library)
  • Runtime: Depends on schema library (Zod, Valibot, Arktype)
  • Memory: No state or caching

Validation overhead varies by schema library:

  • Zod: ~10-50ms for typical schemas
  • Valibot: ~5-20ms (faster)
  • Arktype: ~5-15ms (fastest)

Type Registry

The plugin registers its types using declaration merging:

declare module '@ts-contract/core' {
  interface PluginTypeRegistry<R> {
    validate: {
      validatePathParams: R extends RouteDef
        ? (params: unknown) => InferPathParams<R>
        : never;
      validateQuery: R extends RouteDef
        ? (query: unknown) => InferQuery<R>
        : never;
      validateBody: R extends RouteDef
        ? (body: unknown) => InferBody<R>
        : never;
      validateResponse: R extends RouteDef
        ? <S extends keyof R['responses'] & HttpStatusCodes>(
            status: S,
            data: unknown,
          ) => InferResponseBody<R, S>
        : never;
      validateHeaders: R extends RouteDef
        ? (headers: Record<string, unknown>) => InferHeaders<R>
        : never;
    };
  }
}

This enables full type safety for all validation methods.

Common Patterns

Server-Side Validation

import express from 'express';

app.post('/users', (req, res) => {
  try {
    const body = api.createUser.validateBody(req.body);
    const user = database.createUser(body);
    res.status(201).json(user);
  } catch (error) {
    res.status(400).json({ message: error.message });
  }
});

Client-Side Validation

async function fetchUser(id: string) {
  const response = await fetch(`/users/${id}`);
  const data = await response.json();
  
  if (response.status === 404) {
    const error = api.getUser.validateResponse(404, data);
    throw new Error(error.message);
  }
  
  return api.getUser.validateResponse(200, data);
}

Conditional Validation

const shouldValidate = process.env.NODE_ENV === 'development';

async function fetchUser(id: string) {
  const response = await fetch(`/users/${id}`);
  const data = await response.json();
  
  return shouldValidate
    ? api.getUser.validateResponse(200, data)
    : data;
}

See Also