UI はローカライズ済みです。スキルの紹介と説明文書は選択した言語へ AI が自動翻訳し、コード・コマンド・パス・専門用語は原文のままです。原作者の文書が優先されます。
スキルを探す
開発・技術 · SKILL.md

dev-loop-api-client-generator

dev-loop-api-client-generator

Use when the user asks about generating API clients, OpenAPI/Swagger code generation, REST API client SDKs, or automating API client creation. Do NOT use for: writing API servers, or manual HTTP request code.

Independent GitHub snapshot · Automated classification · Original documentation governs

スキルの説明

GitHub

name: dev-loop-api-client-generator description: > Use when the user asks about generating API clients, OpenAPI/Swagger code generation, REST API client SDKs, or automating API client creation. Do NOT use for: writing API servers, or manual HTTP request code. version: "2.0.0" author: "j4flmao" license: "MIT" compatibility: claude-code: true cursor: true codex: true windsurf: true tags: [dev-loop, api-client, code-generation, openapi]

API Client Generator

Purpose

Generate type-safe, idiomatic API client SDKs from OpenAPI/Swagger specifications, GraphQL schemas, or gRPC protobuf definitions. Automated client generation eliminates manual HTTP code, ensures type safety, and keeps clients synchronized with API changes.

Agent Protocol

Trigger

Exact user phrases: "generate API client", "OpenAPI client", "Swagger codegen", "API SDK generation", "client generator", "auto-generate client", "OpenAPI TypeScript", "openapi-generator", "NSwag", "kiota".

Input Context

  • API specification format (OpenAPI 3.x, Swagger 2.0, GraphQL, gRPC protobuf)
  • Target language/framework (TypeScript, C#, Python, Java, Go, Rust, Kotlin)
  • HTTP client library (fetch, axios, HttpClient, httpx, reqwest)
  • Authentication method (Bearer token, API key, OAuth2, mTLS, custom header)
  • Existing API spec location (URL, file path, registry URL)
  • Generation tool preferences (openapi-generator, NSwag, kiota, or custom)
  • Customization needs (naming conventions, package structure, error handling)

Output Artifact

Generated API client with typed models, service methods, authentication, error handling, and generated code configuration.

Completion Criteria

  • API spec validated and loaded
  • Generation tool selected and configured
  • Client code generated in target language
  • Models/entities typed according to spec schemas
  • Service methods for all API endpoints
  • Authentication configured (bearer token, API key, OAuth2)
  • Error handling and response parsing
  • Request/response interceptors or middleware configured
  • Package/namespace structure defined
  • Generation config committed (can regenerate)
  • Tests for generated client
  • CI regeneration step configured

Max Response Length

250 lines.

Framework/Methodology

Client Generation Decision Tree

What is the API format?
├── OpenAPI 3.x (REST) → openapi-generator, NSwag, kiota
│   ├── TypeScript → openapi-generator (typescript-fetch, typescript-axios)
│   ├── C# → NSwag, kiota, openapi-generator (csharp)
│   ├── Python → openapi-generator (python), kiota
│   ├── Java → openapi-generator (java, spring), kiota
│   ├── Go → openapi-generator (go), oapi-codegen
│   └── Rust → openapi-generator (rust), octorust, paperclip
├── GraphQL → graphql-codegen
│   ├── TypeScript → @graphql-codegen/typescript
│   └── Any → graphql-client (Apollo, urql, Relay)
└── gRPC → protoc + language plugin
    ├── Go → protoc-gen-go-grpc
    ├── C# → Grpc.Tools
    ├── Python → grpcio-tools
    └── Rust → tonic-build

Generation Workflow

API Spec (openapi.yaml / schema.graphql / .proto)
    ↓
Generator Config (language, naming, auth, output dir)
    ↓
Code Generation (openapi-generator CLI, graphql-codegen, protoc)
    ↓
Generated Artifacts (models, API clients, enums, interceptors)
    ↓
Integration (install as package, commit generated code, or build step)
    ↓
CI Validation (diff check, spec change triggers regeneration)

Workflow

Step 1: Configure OpenAPI Generator

# openapi-generator-config.yaml
generatorName: typescript-fetch
inputSpec: ./api/openapi.yaml
outputDir: ./src/generated/api
additionalProperties:
  npmName: "@myorg/api-client"
  npmVersion: "1.0.0"
  typescriptThreePlus: true
  supportsES6: true
  withInterfaces: true
  useSingleRequestParameter: true
  modelPropertyNaming: camelCase
  enumPropertyNaming: UPPERCASE
  ensureUniqueParams: true
  allowUnicodeIdentifiers: false
  prependFormOrBodyParameters: true

Step 2: Generate Client

# OpenAPI Generator CLI
npx @openapitools/openapi-generator-cli generate \
  -g typescript-fetch \
  -i ./api/openapi.yaml \
  -o ./src/generated/api \
  -c openapi-generator-config.yaml

# NSwag (C#)
nswag openapi2csclient /input:openapi.yaml \
  /classname:MyApiClient \
  /namespace:MyApp.Api.Client \
  /output:ApiClient.cs

# Kiota (Microsoft)
kiota generate -l typescript \
  -d openapi.yaml \
  -o ./src/generated/api \
  -n @myorg/api-client

Step 3: Wrap Generated Client

// src/api/client.ts - Typed wrapper around generated client
import { Configuration, DefaultApi, type ApiResponse } from '../generated/api';

export interface ApiClientConfig {
  baseUrl: string;
  tokenProvider: () => Promise<string | null>;
  onError?: (error: Error) => void;
}

export class ApiClient {
  private client: DefaultApi;

  constructor(private config: ApiClientConfig) {
    const apiConfig = new Configuration({
      basePath: config.baseUrl,
      accessToken: config.tokenProvider,
      middleware: [{
        post: async (context): Promise<ApiResponse> => {
          if (!context.response.ok) {
            const error = await this.parseError(context.response);
            config.onError?.(error);
            throw error;
          }
          return context.response as unknown as ApiResponse;
        }
      }]
    });
    this.client = new DefaultApi(apiConfig);
  }

  async getItems(): Promise<Item[]> {
    const response = await this.client.getItems();
    return response;
  }

  async createItem(data: CreateItemRequest): Promise<Item> {
    return this.client.createItem({ createItemRequest: data });
  }

  private async parseError(response: Response): Promise<ApiError> {
    const body = await response.text();
    return new ApiError(
      response.status,
      response.statusText,
      body ? JSON.parse(body) : null
    );
  }
}

Step 4: Authentication Interceptor

// Authentication configuration
export function createAuthMiddleware(tokenProvider: () => Promise<string | null>): Middleware {
  return {
    pre: async (context: RequestContext): Promise<RequestContext> => {
      const token = await tokenProvider();
      if (token) {
        context.init.headers = {
          ...context.init.headers,
          Authorization: `Bearer ${token}`,
        };
      }
      return context;
    }
  };
}

// For API key auth
export function createApiKeyMiddleware(apiKey: string, headerName = 'X-API-Key'): Middleware {
  return {
    pre: async (context: RequestContext): Promise<RequestContext> => {
      context.init.headers = {
        ...context.init.headers,
        [headerName]: apiKey,
      };
      return context;
    }
  };
}

Step 5: Type-Safe Error Handling

export class ApiError extends Error {
  constructor(
    public readonly statusCode: number,
    public readonly statusText: string,
    public readonly body: unknown
  ) {
    super(`API Error ${statusCode}: ${statusText}`);
    this.name = 'ApiError';
  }

  get isNotFound(): boolean { return this.statusCode === 404; }
  get isUnauthorized(): boolean { return this.statusCode === 401; }
  get isForbidden(): boolean { return this.statusCode === 403; }
  get isValidationError(): boolean { return this.statusCode === 422; }
  get isServerError(): boolean { return this.statusCode >= 500; }
  get isRateLimited(): boolean { return this.statusCode === 429; }

  get validationErrors(): ValidationError[] | null {
    return this.isValidationError ? (this.body as any)?.errors ?? null : null;
  }
}

// Usage
try {
  await client.createItem(data);
} catch (error) {
  if (error instanceof ApiError) {
    if (error.isValidationError) {
      // Show form validation errors
      error.validationErrors?.forEach(e => form.setError(e.field, e.message));
    } else if (error.isUnauthorized) {
      // Redirect to login
      authService.logout();
    } else if (error.isRateLimited) {
      // Retry with backoff
      await delay(5000);
      return retry();
    }
  }
}

Step 6: CI Regeneration

# .github/workflows/api-client-update.yml
name: Update API Client
on:
  schedule:
    - cron: '0 6 * * 1'  # Weekly Monday 6AM
  workflow_dispatch:
    inputs:
      spec_url:
        description: 'OpenAPI spec URL'
        required: true
        default: 'https://api.example.com/openapi.json'

jobs:
  generate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
      - name: Generate client
        run: |
          npm run generate-api -- --spec-url ${{ github.event.inputs.spec_url }}
      - name: Check for changes
        id: diff
        run: |
          if git diff --stat --exit-code; then
            echo "no changes"  # No changes needed
          else
            echo "changes=true" >> $GITHUB_OUTPUT
          fi
      - name: Create PR
        if: steps.diff.outputs.changes == 'true'
        run: |
          git checkout -b chore/update-api-client
          git add src/generated/
          git commit -m "chore: update API client from spec"
          gh pr create --title "chore: update API client" --body "Auto-generated from spec"

Common Pitfalls

PitfallDescriptionPrevention
Not pinning generator versionBreaking changes in generatorPin version in package.json or config
Ignoring spec validationGenerating from invalid specValidate spec before generation
Overwriting custom codeManual edits lost on regenerationSeparate generated from hand-written code
No authentication middlewareEvery request repeats auth setupPre-configure middleware/interceptors
Missing nullable/optional typesRuntime errors on undefined valuesUse strict null checks
Committing large generated filesBloated repo, noisy diffs.gitignore generated, build-time generation
Not wrapping generated clientTight coupling to generated APIAbstract behind repository pattern
Ignoring response status codesAll errors treated the sameType-specific error classes
Generated code in source controlStale generated code, merge conflictsGenerate at build time or via CI
No rate limit handling429 responses crash the appRetry with exponential backoff

Best Practices

PracticeRationale
Validate spec before generationCatches schema errors early, before code breaks
Pin generator versionPrevent breaking changes from generator updates
Separate generated and custom codeGenerated code in src/generated/, custom wrappers elsewhere
Abstract behind repository/serviceSwap implementation without changing consumers
Use typed errorsDiscriminated unions or instanceof checks for error types
Generate at build time (not commit)Always fresh, no stale generated code in repo
Set up CI diff checkPR fails if generated code is out of date
Use consistent naming conventionscamelCase for TypeScript, PascalCase for C#
Handle 401/403 globallyRefresh token or redirect to login automatically
Include request/response logging in devDebug API issues, but never log tokens
Use interceptors for cross-cuttingLogging, retry, caching, auth — all in middleware

Templates

OpenAPI Generator Config by Language

# TypeScript (fetch)
generatorName: typescript-fetch
additionalProperties:
  typescriptThreePlus: true
  withInterfaces: true

# C#
generatorName: csharp
additionalProperties:
  packageName: MyOrg.ApiClient
  useDateTimeOffset: true
  targetFramework: net8.0

# Python
generatorName: python
additionalProperties:
  packageName: myorg_api_client
  useOneOfDiscriminatorLookup: true

# Go
generatorName: go
additionalProperties:
  packageName: apiclient
  isGoSubmodule: true

Implementation Patterns

OpenAPI Client Generator

from typing import Dict, List, Optional
import json
import subprocess
from pathlib import Path

class OpenAPIClientGenerator:
    def __init__(self, spec_path: str, output_dir: str):
        self.spec_path = spec_path
        self.output_dir = output_dir
        self.generators = {
            "typescript": {
                "npm_package": "@openapitools/openapi-generator-cli",
                "command": "npx @openapitools/openapi-generator-cli generate",
            },
            "python": {
                "pip_package": "openapi-generator-cli",
                "command": "openapi-generator generate",
            },
        }

    def generate_typescript(self, options: Optional[Dict] = None) -> bool:
        opts = options or {}
        cmd = [
            "npx", "@openapitools/openapi-generator-cli", "generate",
            "-i", self.spec_path,
            "-g", "typescript-axios",
            "-o", f"{self.output_dir}/typescript",
            "--additional-properties=supportsES6=true,withInterfaces=true,useSingleRequestParameter=true",
        ]
        if opts.get("npm_name"):
            cmd.append(f"--additional-properties=npmName={opts['npm_name']}")
        return self._run(cmd)

    def generate_python(self, options: Optional[Dict] = None) -> bool:
        opts = options or {}
        cmd = [
            "openapi-generator", "generate",
            "-i", self.spec_path,
            "-g", "python",
            "-o", f"{self.output_dir}/python",
            "--additional-properties=packageName=api_client",
        ]
        if opts.get("package_name"):
            cmd.append(f"--additional-properties=packageName={opts['package_name']}")
        return self._run(cmd)

    def generate_go(self, options: Optional[Dict] = None) -> bool:
        opts = options or {}
        cmd = [
            "openapi-generator", "generate",
            "-i", self.spec_path,
            "-g", "go",
            "-o", f"{self.output_dir}/go",
            "--additional-properties=packageName=apiclient,isGoSubmodule=true",
        ]
        if opts.get("package_name"):
            cmd.append(f"--additional-properties=packageName={opts['package_name']}")
        return self._run(cmd)

    def _run(self, cmd: List[str]) -> bool:
        try:
            result = subprocess.run(cmd, capture_output=True, text=True)
            if result.returncode == 0:
                print(f"Generated client at {self.output_dir}")
                return True
            print(f"Generation failed: {result.stderr[:200]}")
            return False
        except FileNotFoundError as e:
            print(f"Generator not found: {e}")
            return False

Architecture Decision Trees

Client Generation Strategy

What language/framework?
├── TypeScript
│   ├── axios-based → @openapitools/typescript-axios
│   ├── fetch-based → @openapitools/typescript-fetch
│   ├── Angular → @openapitools/typescript-angular
│   └── Node.js → @openapitools/typescript-node
│
├── Python
│   └── httpx/requests → openapi-generator python
│
├── Go
│   └── net/http → openapi-generator go
│
├── Java
│   ├── Spring → openapi-generator spring
│   └── Retrofit → openapi-generator retrofit
│
└── Custom framework
    └── Use openapi-generator with custom template

Anti-Patterns

Anti-PatternWhy It FailsCorrect Approach
Hand-writing API clientsInconsistent with spec, manual sync neededGenerate from OpenAPI spec
Modifying generated codeChanges lost on regenerationExtend generated client, never modify
No spec validation before generationGenerated client mirrors spec bugsValidate spec with spectral before generation
Stale generated clientsClient doesn't reflect current APICI pipeline regenerates on spec change
Ignoring breaking changes in specProduction breakage on deploymentContract testing between client and spec

Performance Optimization

  • Tree-shake generated clients: Use tree-shaking to remove unused endpoints from generated clients. Reduces bundle size by up to 60% for apps using only a subset of the API.
  • Lazy client initialization: Generate client code in lazy-loaded chunks for large APIs. Dynamically import endpoint modules on first use.

Production Considerations

Version Management

  • Client version pinning: Pin generated client versions to specific API versions. Use semver to communicate breaking changes.
  • Deprecation strategy: Mark endpoints as deprecated in client before removal. Provide migration guide in changelog.
  • Multi-version support: Maintain parallel client versions for APIs with overlapping deprecation windows.

CI/CD Integration

  • Spec-driven generation: Regenerate client on spec change in CI pipeline. Fail build if spec validation fails.
  • Artifact publishing: Publish generated client as versioned package to internal registry. Tag with API version and generation timestamp.
  • Smoke tests: Run generated client against a live API sandbox as part of CI.

Error Handling

  • Retry with backoff: Implement exponential backoff for transient failures. Use jitter to avoid thundering herd.
  • Circuit breaker: Open circuit after N consecutive failures to protect downstream. Close after successful health check.
  • Structured errors: Map HTTP status codes to typed exceptions. Include correlation IDs for debugging.

Security Considerations

Authentication

  • Credential injection: Never hardcode API keys. Use environment variables or secret managers (Vault, AWS Secrets Manager).
  • Token refresh: Handle OAuth2 token refresh transparently. Re-authenticate on 401 responses before retrying.
  • Certificate pinning: Pin TLS certificates in mobile/high-security clients to prevent MITM.

Data Protection

  • Request/response sanitization: Strip sensitive headers (Authorization, Set-Cookie) before logging.
  • Encryption at rest: Encrypt cached API responses containing PII. Use envelope encryption with per-tenant keys.
  • Input validation: Validate all inputs client-side before sending. Prevent injection attacks on string parameters.

Audit & Compliance

  • Request logging: Log all API requests with timestamps, endpoints, and status codes. Exclude sensitive payloads.
  • Rate limit awareness: Respect Retry-After headers. Implement client-side rate limiting to avoid abuse flags.
  • Compliance headers: Add required compliance headers (GDPR consent, data residency) automatically.
保存したスキル