Claude
Skills
Sign in
Back

api-diff-analyzer

Included with Lifetime
$97 forever

Compare API specifications to detect breaking changes. Compare OpenAPI spec versions, categorize changes by severity, generate migration guides, and block breaking changes in CI.

Backend & APIs

What this skill does


# api-diff-analyzer

You are **api-diff-analyzer** - a specialized skill for comparing API specifications and detecting breaking changes, ensuring SDK compatibility and safe API evolution.

## Overview

This skill enables AI-powered API diff analysis including:
- Comparing OpenAPI spec versions
- Categorizing changes by severity
- Detecting breaking changes automatically
- Generating migration guides
- Blocking breaking changes in CI
- Supporting multiple spec formats (OpenAPI, GraphQL, gRPC)
- Creating detailed change reports

## Prerequisites

- OpenAPI, GraphQL, or Protobuf specifications
- Version control with spec history
- oasdiff, openapi-diff, or similar tools
- CI/CD pipeline for automated checks

## Capabilities

### 1. OpenAPI Diff Analysis

Compare OpenAPI specifications:

```typescript
// src/analyzer/openapi-diff.ts
import { parseSpec, diffSpecs } from './parser';

interface ApiChange {
  type: 'breaking' | 'non-breaking' | 'info';
  category: string;
  path: string;
  method?: string;
  description: string;
  oldValue?: unknown;
  newValue?: unknown;
  migration?: string;
}

interface DiffResult {
  hasBreakingChanges: boolean;
  changes: ApiChange[];
  summary: {
    breaking: number;
    nonBreaking: number;
    info: number;
  };
  report: string;
}

export async function analyzeApiDiff(
  oldSpec: string,
  newSpec: string,
  options: DiffOptions = {}
): Promise<DiffResult> {
  const oldApi = await parseSpec(oldSpec);
  const newApi = await parseSpec(newSpec);

  const changes: ApiChange[] = [];

  // Analyze paths
  for (const [path, oldPathItem] of Object.entries(oldApi.paths)) {
    const newPathItem = newApi.paths[path];

    if (!newPathItem) {
      changes.push({
        type: 'breaking',
        category: 'endpoint-removed',
        path,
        description: `Endpoint ${path} was removed`,
        migration: `Update SDK to remove calls to ${path}`
      });
      continue;
    }

    // Analyze methods
    for (const method of ['get', 'post', 'put', 'patch', 'delete']) {
      const oldOp = oldPathItem[method];
      const newOp = newPathItem[method];

      if (oldOp && !newOp) {
        changes.push({
          type: 'breaking',
          category: 'method-removed',
          path,
          method,
          description: `${method.toUpperCase()} ${path} was removed`
        });
        continue;
      }

      if (oldOp && newOp) {
        // Check parameters
        analyzeParameters(path, method, oldOp, newOp, changes);

        // Check request body
        analyzeRequestBody(path, method, oldOp, newOp, changes);

        // Check responses
        analyzeResponses(path, method, oldOp, newOp, changes);
      }
    }
  }

  // Check for new endpoints (non-breaking)
  for (const [path, newPathItem] of Object.entries(newApi.paths)) {
    if (!oldApi.paths[path]) {
      changes.push({
        type: 'non-breaking',
        category: 'endpoint-added',
        path,
        description: `New endpoint ${path} was added`
      });
    }
  }

  // Analyze components/schemas
  analyzeSchemas(oldApi.components?.schemas, newApi.components?.schemas, changes);

  const summary = {
    breaking: changes.filter(c => c.type === 'breaking').length,
    nonBreaking: changes.filter(c => c.type === 'non-breaking').length,
    info: changes.filter(c => c.type === 'info').length
  };

  return {
    hasBreakingChanges: summary.breaking > 0,
    changes,
    summary,
    report: generateReport(changes, summary)
  };
}

function analyzeParameters(
  path: string,
  method: string,
  oldOp: Operation,
  newOp: Operation,
  changes: ApiChange[]
): void {
  const oldParams = new Map(oldOp.parameters?.map(p => [p.name, p]) || []);
  const newParams = new Map(newOp.parameters?.map(p => [p.name, p]) || []);

  // Check for removed parameters
  for (const [name, oldParam] of oldParams) {
    if (!newParams.has(name)) {
      changes.push({
        type: oldParam.required ? 'breaking' : 'info',
        category: 'parameter-removed',
        path,
        method,
        description: `Parameter '${name}' was removed from ${method.toUpperCase()} ${path}`,
        oldValue: oldParam
      });
    }
  }

  // Check for new required parameters
  for (const [name, newParam] of newParams) {
    const oldParam = oldParams.get(name);

    if (!oldParam && newParam.required) {
      changes.push({
        type: 'breaking',
        category: 'required-parameter-added',
        path,
        method,
        description: `New required parameter '${name}' added to ${method.toUpperCase()} ${path}`,
        newValue: newParam,
        migration: `Update SDK calls to include '${name}' parameter`
      });
    }

    if (oldParam && !oldParam.required && newParam.required) {
      changes.push({
        type: 'breaking',
        category: 'parameter-required',
        path,
        method,
        description: `Parameter '${name}' is now required in ${method.toUpperCase()} ${path}`,
        oldValue: oldParam,
        newValue: newParam
      });
    }

    // Check type changes
    if (oldParam && oldParam.schema?.type !== newParam.schema?.type) {
      changes.push({
        type: 'breaking',
        category: 'parameter-type-changed',
        path,
        method,
        description: `Parameter '${name}' type changed from '${oldParam.schema?.type}' to '${newParam.schema?.type}'`,
        oldValue: oldParam,
        newValue: newParam
      });
    }
  }
}

function analyzeSchemas(
  oldSchemas: Record<string, Schema> | undefined,
  newSchemas: Record<string, Schema> | undefined,
  changes: ApiChange[]
): void {
  if (!oldSchemas || !newSchemas) return;

  for (const [name, oldSchema] of Object.entries(oldSchemas)) {
    const newSchema = newSchemas[name];

    if (!newSchema) {
      changes.push({
        type: 'breaking',
        category: 'schema-removed',
        path: `#/components/schemas/${name}`,
        description: `Schema '${name}' was removed`
      });
      continue;
    }

    // Check for removed properties
    if (oldSchema.properties && newSchema.properties) {
      for (const prop of Object.keys(oldSchema.properties)) {
        if (!(prop in newSchema.properties)) {
          changes.push({
            type: 'breaking',
            category: 'property-removed',
            path: `#/components/schemas/${name}/${prop}`,
            description: `Property '${prop}' was removed from schema '${name}'`
          });
        }
      }

      // Check for new required properties
      const oldRequired = new Set(oldSchema.required || []);
      const newRequired = new Set(newSchema.required || []);

      for (const prop of newRequired) {
        if (!oldRequired.has(prop) && oldSchema.properties[prop]) {
          changes.push({
            type: 'breaking',
            category: 'property-required',
            path: `#/components/schemas/${name}/${prop}`,
            description: `Property '${prop}' is now required in schema '${name}'`
          });
        }
      }
    }
  }
}
```

### 2. Breaking Change Categories

Comprehensive breaking change detection:

```typescript
// src/rules/breaking-changes.ts
export const BREAKING_CHANGE_RULES = {
  // Endpoint changes
  'endpoint-removed': {
    severity: 'major',
    description: 'Removing an endpoint breaks all consumers',
    autoFix: false
  },
  'method-removed': {
    severity: 'major',
    description: 'Removing an HTTP method breaks consumers using it',
    autoFix: false
  },

  // Parameter changes
  'required-parameter-added': {
    severity: 'major',
    description: 'Adding required parameter breaks existing calls',
    autoFix: false
  },
  'parameter-removed': {
    severity: 'minor',
    description: 'Removing parameter may break consumers expecting it',
    autoFix: 'Make parameter optional first'
  },
  'parameter-type-changed': {
    severity: 'major',
    description: 'Changing parameter type breaks serialization',
    autoFix: false
  },
  'parameter-required': {
    severity: '

Related in Backend & APIs