Skip to content

Typescript Compiler AST-based generator that creates and syncs a consumer contract version of a provider OpenApi document reflecting what your app actually uses

Notifications You must be signed in to change notification settings

tsirlucas/ts-ast-contracts

Repository files navigation

🧩 Typescript Compiler AST Contract Generator

Generates and syncs a consumer contract version of an OpenApi document reflecting what your app actually uses.


⚠️ Experimental Project

This project is currently experimental and something I'm exploring in my free time.
Expect things to change, break, or be completely rewritten without notice.

There is no guarantee that this project will ever reach a stable or final release —
it may remain a personal experiment indefinitely.

Contributions, discussions, and ideas are welcome — just keep in mind the exploratory nature of the project.

🚨 The Problem

Contract tests help prevent accidental API breakage — but they’re expensive to write and maintain.
Most applications only use a subset of their provider’s API surface, thus traditional contract tests require you to manually:

  • Track every endpoint your code touches
  • Track every property your code uses
  • Maintain fixtures and mocks for each
  • Update them every time the API or client evolves

Wouldn’t it be better if your code told you what it used automatically?


💡 The Idea

This tool reads your TypeScript project and your provider’s OpenAPI spec, then automatically generates a “used-only” OpenAPI that reflects what your app actually calls and reads.

You can then use standard OpenAPI diff tools (like oasdiff or pb33f/openapi-changes) to catch real breaking changes — and ignore noise.


🧱 Overview

Scenario

flowchart TB
 subgraph Testing["Testing"]
    direction TB
        fe_test_1["frontend-app #1<br>(uses id, email)"]
        fe_test_2["frontend-app #2<br>(uses id)"]
        be_test_1["backend-service<br>(removes email)"]
  end
 subgraph Staging["Staging"]
    direction TB
        fe_stg_1["frontend-app #1<br>(uses id, email)"]
        fe_stg_2["frontend-app #2<br>(uses id)"]
        be_stg_1["backend-service<br>(removes email)"]
  end
    Staging ~~~ Testing
    be_stg_1 --> be_test_1
     fe_test_1:::envred
     fe_test_2:::envgreen
     be_test_1:::envgreen
     fe_stg_1:::envgreen
     fe_stg_2:::envgreen
     be_stg_1:::envgreen
    classDef envgreen fill:#1aad57,stroke:#222,stroke-width:2px,color:#fff
    classDef envred fill:#f23a3d,stroke:#222,stroke-width:2px,color:#fff
Loading

“Zero-maintenance contracts” pipeline

flowchart LR
  TS["Your TypeScript Codebase"] --- AST["TypeScript AST & Type Checker"]
  OAS["Provider OpenAPI (full)"]
  AST -->|used types, endpoints, props| FILT["Usage-aware filter"]
  OAS --> FILT
  FILT --> OAS_SUB["Used-only OpenAPI"]
  OAS_SUB --> DIFF["Diff Tool<br/>(oasdiff, pb33f/openapi-changes)"]
  OAS --> DIFF
  DIFF --> CI["CI status<br/>(breaking? ❌ : ✅)"]
Loading

⚙️ How It Works

  1. Load your project
    Uses ts-morph and the TypeScript compiler API to inspect your source files, respecting your tsconfig.json.

  2. Analyze usage
    Traverses the AST and collects all symbols/types that appear in:

    • Variable property access (regular access, destruction, etc)
    • Function arguments (only whats required in parameters)
    • JSX props (same as functions arguments)
  3. Resolve to OpenAPI entities
    Matches your used types to schemas, operations, and parameters in the provided OpenAPI spec.

  4. Prune unused parts
    Removes everything not directly or transitively used by your app — leaving a used-only spec.

  5. Diff
    Run oasdiff or openapi-changes between the provider spec and your generated used-only spec:

    • Breaking: removed or modified properties you actually use
    • Ignored: removed or modified properties you never touch

🧪 Example

Provider’s OpenAPI

openapi: 3.0.0
info:
  title: Provider API
  version: 1.0.0

paths:
  /users/{id}:
    get:
      responses:
        "200":
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/User"

components:
  schemas:
    User:
      type: object
      required:
        - id
        - email
      properties:
        id:
          type: string
        email:
          type: string
        displayName:
          type: string
        marketingOptIn:
          type: boolean

Your TypeScript code

export async function run(id: string) {
  const user = await userClient.getUser(id) // Returns a 'User'
  console.log(user.email); // only 'email' is used
}

Generated “used-only” spec

openapi: 3.0.0
info:
  title: Provider API (used-only)
  version: 1.0.0

paths:
  /users/{id}:
    get:
      responses:
        "200":
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/User"

components:
  schemas:
    User:
      type: object
      required:
        - email
      properties:
        email:
          type: string

🧰 Usage

CLI

# 1️⃣ Install
npm i -D contract-usage-openapi

# 2️⃣ Generate used-only spec
npx contract-usage-openapi   --tsconfig ./tsconfig.json   --entry ./src   --input-openapi ./provider.yaml   --output-openapi ./used-only.yaml

# 3️⃣ Diff
npx oasdiff diff ./provider.yaml ./used-only.yaml --fail-on-breaking

🔬 Guarantees

✅ Real type-checked usage (no regex guessing)
✅ Attempts to detect real usage, fallsback to "will typescript compiler break if this is removed/changed?"
🚫 Doesn’t mock APIs — it only generates specs
🚫 Casted (obj[prop as string]) or untyped access will be ignored


🧭 Roadmap

  • Basic property accesses (regular, destruction, etc)
  • Basic top-level argument vs parameter usage detection
  • Identify nested types in argument vs parameter usage detection
  • Basic union types
  • Cross-file checks
  • Dont mix up types with same structure
  • Remove unused endpoints
  • Remove unused properties
  • Better generics support
  • Object spread support
  • Perf improvements
  • JSX support
  • Handle union arguments
  • Handle intersection arguments
  • Better union/intersection spread in component props
  • Add more tests for element access around component props and spread
  • Decide if optional param properties should consider argument property as used
  • Handle request body params
  • Handle request query params
  • Add workaround for proxied methods
  • Multiple generics support
  • Single forEachDescendant call (perf improvement)
  • Nested OpenApi schemas (still deciding if its worth suporting it)
  • Much more

🧑‍💻 Contributing

PRs and issues are welcome!
If you find an unsupported use-case, please open a PR adding a test.

About

Typescript Compiler AST-based generator that creates and syncs a consumer contract version of a provider OpenApi document reflecting what your app actually uses

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published