Building Type-Safe APIs with itty-spec: A Contract-First Approach

Published: (December 14, 2025 at 04:43 PM EST)
5 min read
Source: Dev.to

Source: Dev.to

Introduction

itty-spec is a powerful library that brings type‑safe, contract‑first API development to itty‑router. By defining your API contracts using standard schema libraries, you get:

  • Automatic validation
  • Full TypeScript type inference
  • Seamless OpenAPI documentation generation
  • Compatibility with edge computing environments

Traditional API development often suffers from:

  • Manual validation scattered across route handlers
  • Type definitions that drift from actual runtime behavior
  • Inconsistent error handling for invalid requests
  • Outdated documentation that requires manual maintenance
  • No runtime guarantee that handlers match their documented contracts

These issues lead to bugs, security vulnerabilities, and a poor developer experience. itty-spec solves them by making schema definitions the single source of truth for routes, validation, types, and documentation.

Contract‑First Approach

itty-spec follows a contract‑first workflow:

  • Route registration – automatically creates routes from contract definitions
  • Request validation – validates all incoming data against schemas
  • Type inference – provides full TypeScript types for handlers
  • Response validation – ensures handlers return valid responses
  • Documentation generation – automatically creates OpenAPI specifications

Typed Request Objects

When you define a contract, TypeScript automatically infers the types for:

  • Path parameters – extracted from patterns like /users/:id
  • Query parameters – typed and validated query strings
  • Request headers – validated header objects
  • Request body – typed request payloads
import { createContract } from 'itty-spec';
import { z } from 'zod';

const contract = createContract({
  getUser: {
    path: '/users/:id',
    query: z.object({
      include: z.enum(['posts', 'comments']).optional(),
    }),
    headers: z.object({
      'x-api-key': z.string(),
    }),
    responses: {
      200: { body: z.object({ id: z.string(), name: z.string() }) },
    },
  },
});
// In your handler, everything is fully typed!
const router = createRouter({
  contract,
  handlers: {
    getUser: async (request) => {
      // request.params.id is typed as string
      // request.query.include is typed as 'posts' | 'comments' | undefined
      // request.validatedHeaders['x-api-key'] is typed as string

      const userId = request.params.id; // string
      const include = request.query.include; // enum | undefined

      return request.json({ id: userId, name: 'John' });
    },
  },
});

The type system prevents accidental access to non‑existent properties and offers autocomplete for all available fields.

Validation Pipeline

itty-spec uses a middleware pipeline that validates incoming requests before they reach your handlers. Validation occurs in this order:

  1. Path parameters – extracted and validated against an optional pathParams schema
  2. Query parameters – parsed and validated
  3. Headers – normalized and validated
  4. Request body – parsed from JSON and validated (for POST/PUT/PATCH)

If any step fails, the request is rejected with a 400 status code and detailed error information, never reaching your handler code.

const contract = createContract({
  createUser: {
    path: '/users',
    method: 'POST',
    request: z.object({
      name: z.string().min(1).max(100),
      email: z.string().email(),
      age: z.number().int().min(18).max(120),
    }),
    responses: {
      200: { body: z.object({ id: z.string(), name: z.string() }) },
      400: { body: z.object({ error: z.string(), details: z.any() }) },
    },
  },
});

When the request body doesn’t match the schema, validation fails automatically, allowing your handler to focus solely on business logic.

Vendor‑Agnostic Schema Support

itty-spec leverages the Standard Schema V1 specification, enabling you to use any schema library that implements it:

  • Zod (v4) – fully supported with OpenAPI generation
  • Valibot – compatible with validation (OpenAPI support planned)
  • ArkType – compatible with validation (OpenAPI support planned)
  • Any other library implementing Standard Schema V1

The spec defines a unified interface (StandardSchemaV1) with:

  • ~standard.vendor – identifies the schema library
  • ~standard.validate() – standardized validation method
  • Type inference capabilities through TypeScript
// Works with Zod
import { z } from 'zod';
const zodSchema = z.object({ name: z.string() });

// Works with Valibot (once supported)
import * as v from 'valibot';
const valibotSchema = v.object({ name: v.string() });

// Both can be used in contracts
const contract = createContract({
  endpoint: {
    path: '/test',
    request: zodSchema, // or valibotSchema
    responses: { 200: { body: zodSchema } },
  },
});

This approach prevents lock‑in to a specific library and simplifies migration.

Automatic OpenAPI 3.1 Generation

itty-spec can generate a complete OpenAPI 3.1 specification directly from your contracts, eliminating manual documentation upkeep.

Key features of the OpenAPI generator

  • Extracts schemas from all contract operations
  • Converts :param path syntax to {param} (OpenAPI standard)
  • Maps response schemas to OpenAPI response objects
  • Includes headers, query params, and request bodies
  • Deduplicates schemas via a registry system
import { createOpenApiSpecification } from 'itty-spec/openapi';
import { contract } from './contract';

const openApiSpec = createOpenApiSpecification(contract, {
  title: 'User Management API',
  version: '1.0.0',
  description: 'A comprehensive API for managing users',
  servers: [
    { url: 'https://api.example.com', description: 'Production' },
    { url: 'https://staging-api.example.com', description: 'Staging' },
  ],
});

The generated spec can be used for:

  • Interactive documentation (Swagger UI, Stoplight Elements)
  • Client SDK generation (openapi-generator, swagger-codegen)
  • API testing tools (Postman, Insomnia)
  • CI/CD validation

The output complies fully with OpenAPI 3.1 and works with any supporting tool.

Typed Response Helpers

itty-spec adds typed helper methods on the request object to ensure responses adhere to contract definitions:

  • request.json(body, status?, headers?) – JSON responses with type validation
  • request.html(html, status?, headers?) – HTML responses
  • request.error(status, body, headers?) – Error responses
  • request.noContent(status) – 204 No Content responses

These helpers enforce:

  • Only valid status codes defined in the contract can be used
  • Response bodies match the schema for the given status code
  • Headers conform to any optional header schema
const contract = createContract({
  getUser: {
    path: '/users/:id',
    responses: {
      200: { body: z.object({ id: z.string(), name: z.string() }) },
      404: { body: z.object({ error: z.string() }) },
    },
  },
});

const router = createRouter({
  contract,
  handlers: {
    getUser: async (request) => {
      const user = await findUser(request.params.id);

      if (!user) {
        // TypeScript ensures this matches the 404 response schema
        return request.error(404, { error: 'User not found' });
      }

      return request.json({ id: user.id, name: user.name });
    },
  },
});

Conclusion

By making schemas the single source of truth, itty-spec delivers:

  • Type safety across the entire request‑response lifecycle
  • Automatic validation that protects against malformed inputs
  • Zero‑maintenance documentation via OpenAPI generation
  • Flexibility to choose any Standard Schema V1‑compatible library

Adopt itty-spec to streamline API development, reduce bugs, and improve developer experience in edge‑ready environments.

Back to Blog

Related posts

Read more »