typed-openapi

repository·main·Indexed 43 days ago

https://github.com/astahmer/typed-openapi

A tool that generates a fully type-safe TypeScript API client from an OpenAPI specification. It supports runtime validation libraries including Zod, Effect, and Valibot, provides seamless TanStack Query integration, and can generate MSW mocks for testing.

Tokens
37.7K
Snippets
116
Records
149
Agent score
56%

What's inside typed-openapi

  1. Overview of typed-openapi

    main

    typed-openapi is a tool designed to generate type-safe TypeScript API clients, TypeScript types, and runtime validators directly from an OpenAPI specification document.

    It is designed to be flexible regarding how you consume the generated code:

    • Types-only: Generate paths, parameters, responses, and endpoint contracts without adding runtime dependencies, allowing you to keep your existing HTTP layer.
    • Runtime Validation: Generate validators using your preferred library (Zod, Effect, Valibot, ArkType, TypeBox, or Typia). It uses a 'runtime type sidecar' approach where validator implementations are kept in sibling declaration files to prevent large validator logic from slowing down TypeScript editor inference.
    • Client Generation: Generate Fetch-based clients or integrate with other data layers like TanStack Query, Promise, or Effect.
    • Specification Filtering: For large API specifications, you can filter the spec to generate a smaller, domain-specific client.
  2. Explore typed-openapi developer resources

    main

    The following entry points are available for integrating typed-openapi or navigating its features:

    • CLI reference: Documentation for every command-line option and generated output control.
    • Library API: Information on programmatic generation for custom developer tooling.
    • Promise client and fetcher: Details on generated Fetch clients and custom transports.
    • Request bodies, cookies, and auth: Guidance on request encoding and security schemes.
    • Runtime adapters: Support for Zod, Effect, Valibot, ArkType, TypeBox, and Typia.
    • AI agent guide: Instructions for repository skill and focused integration workflows.
    • llms.txt: A machine-readable documentation index.
    • sitemap.xml: A machine-readable URL list.
  3. Best practices and rules for typed-openapi

    main

    To maintain a healthy generated codebase, adhere to these rules:

    • Spec-First: Always inspect the OpenAPI document, typed-openapi.config.*, and existing generated files before making changes. Change the spec or configuration to drive changes, rather than hand-writing endpoint types or editing generated output.
    • Types vs. Runtime: Use types-only output by default. Only add runtime validation if the consuming application already uses it or explicitly requires runtime boundary checks.
    • Transport Management: Treat the --default-fetcher as generated transport code. If an application already manages its own retry logic, authentication, tracing, or environment-specific configuration, keep the existing transport instead of using the default.
    • Conditional Generation: Only generate TanStack Query and MSW companions in packages that actually depend on those libraries.
  4. Understand type-check benchmark multipliers

    main

    When evaluating the impact of typed-openapi on your development workflow, use the Usage-to-baseline multiplier to understand the additional compiler work required by typed API-client calls.

    • Higher values indicate that using the generated API client adds significant work to the TypeScript compiler beyond simply importing the generated declarations.
    • Lower values (closer to 1.0×) indicate that the runtime/schema library used has minimal impact on type-checking performance when integrated with the client.

    Note on interpreting results: Check-time ratios can be sensitive to machine noise. When evaluating performance changes, prioritize instantiation trends and use the five-run medians provided in the full benchmark reports.

  5. Handle API responses with Direct or Union-style modes

    main

    The client supports two response styles for handling API results:

    1. Direct data return (default): The method returns the typed data directly. If the response status is an error, it throws a TypedStatusError (which includes the parsed error data).

      const user = await api.get("/users/{id}", { path: { id: "123" } });
    2. Union-style response (withResponse: true): The method returns a response object. This prevents throwing and allows you to check result.ok to handle success or error branches manually.

      const result = await api.get("/users/{id}", { path: { id: "123" }, withResponse: true });
      if (result.ok) {
        // result.data is typed as User
      } else {
        // result.data is typed as your error schema for that status
      }

    All errors thrown by the client are instances of TypedStatusError and include the parsed error data.

    // Direct data return (default)
    const user = await api.get("/users/{id}", { path: { id: "123" } });
    
    // Union-style response
    const result = await api.get("/users/{id}", { path: { id: "123" }, withResponse: true });
    if (result.ok) {
      // result.data is typed as User
    } else {
      // result.data is typed as your error schema for that status
    }
  6. Understand the ApiError Response-like structure

    main

    All errors thrown by TanStack Query mutations follow a consistent structure that extends the native Response object. This ensures that error handling is predictable and integrates well with standard web APIs.

    An error object (often referred to as ApiError) includes:

    • status: The HTTP status code (e.g., 400, 409, 500), which is properly typed as a union of the error codes defined in your OpenAPI spec.
    • data: The typed error response body parsed from the JSON response.
    • ok: A boolean indicating if the response was successful.
    • headers: The Headers object from the response.
    • url: The request URL.

    Because they are Response instances, you can use error instanceof Response to verify them at runtime.

    interface ApiError<TData = unknown> extends Omit<Response, "status"> {
      status: number; // HTTP status code (400-511) - properly typed
      data: TData; // Typed error response body
      // All other Response properties are available:
      // ok: boolean
      // headers: Headers
      // url: string
      // etc.
    }
  7. Handle request bodies and encoding with `requestFormat`

    main

    The generated client uses a requestFormat signal to tell the fetcher how to encode the request body. The default fetcher automatically handles the following mappings based on the OpenAPI content-type:

    OpenAPI content typerequestFormatGenerated behavior
    application/jsonjsonJSON.stringify()
    multipart/form-dataform-dataFormData and browser-provided boundary
    application/x-www-form-urlencodedform-urlURLSearchParams
    application/octet-streambinaryraw Blob, buffer, bytes, or string
    text/plaintextString(body)

    Important: If you implement a custom fetcher, you must preserve the requestFormat signal to ensure the body is encoded correctly according to the OpenAPI specification.

  8. Understand playground output files

    main

    The output tabs in the playground mirror the default filenames generated by the library.

    • Main files: A runtime creates openapi.<runtime>.ts and its sidecar openapi.<runtime>.types.d.ts.
    • Additional files: Enabling specific options will generate:
      • Default fetcher: api.client.ts
      • TanStack Query: tanstack.client.ts
      • MSW handlers: msw.handlers.ts
  9. Understand OpenAPI field behaviors (Defaults, Recursion, and Read/Write)

    main

    The generator preserves the declared OpenAPI contract with the following behaviors:

    • Default Values: Runtime adapters emit OpenAPI default values.
    • Recursive Schemas: Recursive component schemas utilize the specific adapter's lazy/suspend strategy.
    • Read/Write Fields:
      • Inline request objects automatically omit readOnly fields.
      • Inline response objects automatically omit writeOnly fields.
    • Shared Schemas: Named $ref schemas remain shared across different endpoint contexts.

    Note: If an endpoint lacks type detail (e.g., missing response schema or using a generic object), the generator cannot recover it. You must improve the OpenAPI specification itself to get better types.

  10. Understanding the `--default-fetcher` behavior

    main

    Using the --default-fetcher flag generates a second TypeScript file that acts as the transport layer.

    Key behaviors:

    • Parameter Handling: It replaces path parameters, serializes query parameters, and adds OpenAPI cookie/header parameters before execution.
    • Body Encoding: It encodes POST/PUT/PATCH/DELETE bodies based on the requestFormat contract.
    • Overrides: It forwards request overrides and returns a native Response object.
    • Client Parsing: The generated API client is responsible for parsing the returned Response.
    • Initialization: It exports createApi(baseUrl), allowing browser applications to provide a bundler-specific URL without modifying generated code.
    • Base URL: It reads globalThis.process?.env?.API_BASE_URL by default, falling back to https://api.example.com.
    • Authentication: If the OpenAPI spec defines securitySchemes, you must call configureFetcher({ getAuth }) before making the first request. The fetcher selects only the first satisfied security requirement and will not send credentials to operations that declare no security requirements.
  11. Configure companion output paths

    main

    Companion outputs such as tanstack, msw, or a string value for defaultFetcher are resolved relative to the main output file's directory, not the project root or the config file.

    Example: If your main output is ./src/api/openapi.ts:

    • To output to ./src/api/query.ts, use tanstack: "query.ts".
    • To output to ./src/mocks/handlers.ts, use msw: "../mocks/handlers.ts".
  12. How the Runtime Declaration Sidecar works

    main

    By default, runtime clients in typed-openapi generate a sibling .types.d.ts file (a "sidecar"). This sidecar contains the public schema aliases and API-client route types.

    To optimize TypeScript performance, the generated runtime module uses // @ts-nocheck to skip semantic checking of the large validator implementation. This ensures that while the runtime validation logic executes normally, TypeScript does not waste time checking the internal expressions of the validator. Consumers still benefit from strict type safety because they consume types from the .types.d.ts sidecar, not the unchecked runtime module.

    Important Requirements:

    • File Proximity: You must keep the generated .types.d.ts file alongside its corresponding runtime module when copying or publishing generated output.
    • TypeScript Configuration: For optimal performance, ensure skipLibCheck is enabled in your tsconfig.json. Without it, TypeScript will attempt to type-check the large declaration sidecar, negating the performance benefits.