Project 39: “The API Client Generator” — Integration

Attribute Value
File KIRO_CLI_LEARNING_PROJECTS.md
Main Programming Language TypeScript / Python
Coolness Level Level 2: Practical
Difficulty Level 2: Intermediate
Knowledge Area Integration

What you’ll build: An OpenAPI-to-SDK generator that reads openapi.yaml specifications and produces fully-typed client libraries in TypeScript or Python with request/response validation, error handling, and authentication support.

Why it teaches Automation: This project eliminates the manual work of writing API client boilerplate. You’ll learn how to use Kiro to generate production-ready SDKs that stay in sync with your API spec.

Core challenges you’ll face:

  • OpenAPI spec parsing → Maps to YAML/JSON parsing, JSON Schema validation
  • Code generation → Maps to template engines, AST builders
  • Type safety → Maps to TypeScript interfaces from JSON Schema
  • Authentication patterns → Maps to API key, Bearer token, OAuth2 flow implementation

Real World Outcome

You’ll have a CLI tool that transforms an OpenAPI spec into a production-ready, typed SDK:

$ cat openapi.yaml
openapi: 3.0.0
info:
  title: Task Management API
  version: 1.0.0
servers:
  - url: https://api.example.com/v1
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
  schemas:
    Task:
      type: object
      required: [id, title, status]
      properties:
        id:
          type: string
          format: uuid
        title:
          type: string
          minLength: 1
          maxLength: 200
        description:
          type: string
          nullable: true
        status:
          type: string
          enum: [todo, in_progress, done]
        dueDate:
          type: string
          format: date-time
          nullable: true
        tags:
          type: array
          items:
            type: string
paths:
  /tasks:
    get:
      summary: List all tasks
      security:
        - bearerAuth: []
      parameters:
        - name: status
          in: query
          schema:
            type: string
            enum: [todo, in_progress, done]
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            minimum: 1
            maximum: 100
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  tasks:
                    type: array
                    items:
                      $ref: '#/components/schemas/Task'
                  total:
                    type: integer
    post:
      summary: Create a task
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [title]
              properties:
                title:
                  type: string
                description:
                  type: string
                status:
                  type: string
                  enum: [todo, in_progress, done]
                  default: todo
      responses:
        '201':
          description: Task created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Task'
  /tasks/{taskId}:
    get:
      summary: Get task by ID
      security:
        - bearerAuth: []
      parameters:
        - name: taskId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Task'

$ kiro generate-sdk --spec openapi.yaml --language typescript --output task-api-client

[Kiro CLI Session]
📦 API Client Generator
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Step 1: Parsing OpenAPI spec...
  ✓ OpenAPI version: 3.0.0
  ✓ API title: Task Management API
  ✓ Base URL: https://api.example.com/v1
  ✓ Security: Bearer Token (HTTP)
  ✓ Schemas: 1 (Task)
  ✓ Endpoints: 3 (GET /tasks, POST /tasks, GET /tasks/{taskId})

Step 2: Generating TypeScript types...
  ✓ Generated: src/types.ts
  ✓ Generated: src/schemas.ts (Zod validators)

Step 3: Generating API client...
  ✓ Generated: src/client.ts
  ✓ Generated: src/endpoints/TasksApi.ts

Step 4: Generating tests...
  ✓ Generated: tests/client.test.ts

Step 5: Setting up project...
  ✓ package.json created
  ✓ tsconfig.json created
  ✓ README.md created

✅ SDK generated successfully!

$ tree task-api-client/
task-api-client/
├── src/
│   ├── client.ts          (Main API client class)
│   ├── types.ts           (TypeScript interfaces)
│   ├── schemas.ts         (Zod runtime validators)
│   └── endpoints/
│       └── TasksApi.ts    (Task endpoints wrapper)
├── tests/
│   └── client.test.ts
├── package.json
├── tsconfig.json
└── README.md

$ cat task-api-client/src/types.ts
// Auto-generated from openapi.yaml — DO NOT EDIT

export interface Task {
  id: string;           // UUID
  title: string;        // 1-200 chars
  description?: string | null;
  status: 'todo' | 'in_progress' | 'done';
  dueDate?: string | null;  // ISO 8601 date-time
  tags?: string[];
}

export interface ListTasksRequest {
  status?: 'todo' | 'in_progress' | 'done';
  limit?: number;  // 1-100, default: 20
}

export interface ListTasksResponse {
  tasks: Task[];
  total: number;
}

export interface CreateTaskRequest {
  title: string;
  description?: string;
  status?: 'todo' | 'in_progress' | 'done';
}

$ cat task-api-client/src/client.ts
import axios, { AxiosInstance } from 'axios';
import { TasksApi } from './endpoints/TasksApi';

export class TaskApiClient {
  private axiosInstance: AxiosInstance;
  public tasks: TasksApi;

  constructor(config: {
    baseURL?: string;
    apiKey: string;
    timeout?: number;
  }) {
    this.axiosInstance = axios.create({
      baseURL: config.baseURL || 'https://api.example.com/v1',
      timeout: config.timeout || 30000,
      headers: {
        'Authorization': `Bearer ${config.apiKey}`,
        'Content-Type': 'application/json',
      },
    });

    // Initialize endpoint wrappers
    this.tasks = new TasksApi(this.axiosInstance);
  }
}

$ cat task-api-client/src/endpoints/TasksApi.ts
import { AxiosInstance } from 'axios';
import {
  Task,
  ListTasksRequest,
  ListTasksResponse,
  CreateTaskRequest,
} from '../types';
import { TaskSchema, ListTasksResponseSchema } from '../schemas';

export class TasksApi {
  constructor(private axios: AxiosInstance) {}

  /**
   * List all tasks
   * @param params - Query parameters
   * @returns List of tasks with total count
   */
  async list(params?: ListTasksRequest): Promise<ListTasksResponse> {
    const response = await this.axios.get<ListTasksResponse>('/tasks', {
      params,
    });

    // Runtime validation with Zod
    return ListTasksResponseSchema.parse(response.data);
  }

  /**
   * Create a new task
   * @param data - Task creation payload
   * @returns The created task
   */
  async create(data: CreateTaskRequest): Promise<Task> {
    const response = await this.axios.post<Task>('/tasks', data);
    return TaskSchema.parse(response.data);
  }

  /**
   * Get task by ID
   * @param taskId - Task UUID
   * @returns The task details
   */
  async get(taskId: string): Promise<Task> {
    const response = await this.axios.get<Task>(`/tasks/${taskId}`);
    return TaskSchema.parse(response.data);
  }
}

$ cat task-api-client/src/schemas.ts
import { z } from 'zod';

export const TaskSchema = z.object({
  id: z.string().uuid(),
  title: z.string().min(1).max(200),
  description: z.string().nullable().optional(),
  status: z.enum(['todo', 'in_progress', 'done']),
  dueDate: z.string().datetime().nullable().optional(),
  tags: z.array(z.string()).optional(),
});

export const ListTasksResponseSchema = z.object({
  tasks: z.array(TaskSchema),
  total: z.number().int(),
});

$ cat task-api-client/README.md
# Task Management API Client

Auto-generated TypeScript SDK for Task Management API.

## Installation

```bash
npm install task-api-client

Usage

import { TaskApiClient } from 'task-api-client';

const client = new TaskApiClient({
  apiKey: process.env.API_KEY!,
});

// List tasks
const { tasks, total } = await client.tasks.list({
  status: 'todo',
  limit: 10,
});

// Create a task
const newTask = await client.tasks.create({
  title: 'Implement user authentication',
  description: 'Add JWT-based auth flow',
  status: 'todo',
});

// Get task by ID
const task = await client.tasks.get(newTask.id);

Features

✓ Full TypeScript type safety ✓ Runtime validation with Zod ✓ Automatic authentication (Bearer token) ✓ Error handling and retries ✓ Request/response interceptors ✓ Auto-generated from OpenAPI 3.0 spec

$ cd task-api-client && npm install && npm test

task-api-client@1.0.0 test jest

PASS tests/client.test.ts TaskApiClient ✓ creates client with API key (12ms) ✓ lists tasks with filters (45ms) ✓ creates a new task (34ms) ✓ validates response schema (23ms) ✓ throws error on invalid status (18ms)

Tests: 5 passed, 5 total Time: 2.134s

✅ SDK is ready to publish!


**Exactly what happens:**
1. Kiro parses the OpenAPI spec and extracts schemas, endpoints, and auth requirements
2. It generates TypeScript interfaces from JSON Schema definitions
3. It creates Zod validators for runtime type safety
4. It generates an API client class with typed methods for each endpoint
5. It adds authentication, error handling, and request validation
6. It produces a complete npm package ready to publish

#### The Core Question You're Answering

> "How do you automatically generate production-ready API clients that stay in sync with your OpenAPI spec and provide full type safety?"

This is about code generation as a force multiplier:
- Eliminating manual SDK maintenance (API changes → regenerate SDK)
- Providing better DX than hand-written clients (types, validation, docs)
- Ensuring client and server contracts match (single source of truth)

#### Concepts You Must Understand First

**Stop and research these before coding:**

1. **OpenAPI Specification 3.0**
   - What are paths, operations, parameters, requestBody, responses?
   - How do JSON Schema definitions map to types?
   - What are `$ref` references and how do you resolve them?
   - *Reference:* OpenAPI 3.0 Specification (https://spec.openapis.org/oas/v3.0.3)

2. **Code Generation Strategies**
   - Template engines (Handlebars, EJS) vs AST builders (TypeScript Compiler API)
   - When to use string concatenation vs structured code generation?
   - How do you generate readable, idiomatic code?
   - *Book Reference:* "Code Generation in Action" by Jack Herrington

3. **Runtime Validation**
   - Why use Zod, Yup, or io-ts for runtime type checking?
   - What's the difference between compile-time types (TypeScript) and runtime validation?
   - How do you handle optional fields, nullable types, and unions?
   - *Reference:* Zod documentation (https://zod.dev)

4. **HTTP Client Patterns**
   - Axios vs Fetch API — which to use for generated clients?
   - How do you handle authentication (API keys, Bearer tokens, OAuth2)?
   - What about request interceptors, retries, and error handling?
   - *Book Reference:* "RESTful Web API Patterns & Practices" by Mike Amundsen

#### Questions to Guide Your Design

**Before implementing, think through these:**

1. **Spec Parsing**
   - How do you resolve `$ref` pointers to schemas in other files?
   - What if the spec has circular references (Task → User → Task)?
   - Should you validate the OpenAPI spec before generating code?
   - How do you handle deprecated endpoints or parameters?

2. **Type Generation**
   - Should you generate interfaces or types? (`interface Task` vs `type Task`)
   - How do you handle discriminated unions (polymorphic schemas)?
   - What about enums — should they be TypeScript enums or union types?
   - How do you generate JSDoc comments from OpenAPI descriptions?

3. **SDK Structure**
   - Should you group endpoints by tags or paths?
   - Should each endpoint be a method or a separate class?
   - How do you handle pagination, filtering, sorting?
   - What about file uploads (multipart/form-data)?

4. **Versioning and Updates**
   - If the API spec changes, how do you regenerate without breaking client code?
   - Should you version the SDK independently of the API?
   - How do you handle breaking vs non-breaking changes?
   - Should the generator produce a diff showing what changed?

#### Thinking Exercise

### Type Generation Challenge

You have this OpenAPI schema:
```yaml
components:
  schemas:
    User:
      type: object
      required: [id, email, role]
      properties:
        id:
          type: string
        email:
          type: string
          format: email
        role:
          type: string
          enum: [admin, member, guest]
        profile:
          oneOf:
            - $ref: '#/components/schemas/AdminProfile'
            - $ref: '#/components/schemas/MemberProfile'
    AdminProfile:
      type: object
      properties:
        permissions:
          type: array
          items:
            type: string
    MemberProfile:
      type: object
      properties:
        joinedAt:
          type: string
          format: date-time

Questions to reason through:

  1. Should role be a TypeScript enum or a union type ('admin' | 'member' | 'guest')?
  2. How do you represent oneOf in TypeScript? (Union type? Discriminated union?)
  3. The profile field depends on role — can you enforce this at the type level?
  4. Should you generate separate types for AdminProfile and MemberProfile or inline them?
  5. How would Zod validate the oneOf relationship at runtime?

Generated types:

export type UserRole = 'admin' | 'member' | 'guest';

export interface AdminProfile {
  permissions: string[];
}

export interface MemberProfile {
  joinedAt: string;  // ISO 8601 date-time
}

export interface User {
  id: string;
  email: string;
  role: UserRole;
  profile: AdminProfile | MemberProfile;
}

Is this type-safe enough? How would you improve it?

The Interview Questions They’ll Ask

Prepare to answer these:

  1. “How would you handle versioning in a generated SDK? (e.g., API v1 vs v2)”
  2. “Your OpenAPI spec has a circular reference. How do you generate types without infinite loops?”
  3. “What’s the difference between compile-time type safety and runtime validation? Why do you need both?”
  4. “How would you handle authentication in a generated client? (API keys, OAuth2, etc.)”
  5. “The API spec changes frequently. How do you keep the SDK in sync without manual work?”
  6. “Should you generate one giant SDK or multiple packages per API resource?”

Hints in Layers

Hint 1: Parse OpenAPI with a Library Don’t parse YAML manually — use a library:

import SwaggerParser from '@apidevtools/swagger-parser';

const api = await SwaggerParser.dereference('openapi.yaml');
// This resolves all $ref pointers into a single object

const paths = api.paths;
const schemas = api.components.schemas;

Hint 2: Generate Types from JSON Schema For each schema in components.schemas:

function generateInterface(name: string, schema: any): string {
  const required = schema.required || [];
  const properties = Object.entries(schema.properties || {})
    .map(([key, prop]: [string, any]) => {
      const optional = !required.includes(key) ? '?' : '';
      const type = mapJsonSchemaTypeToTS(prop);
      return `  ${key}${optional}: ${type};`;
    })
    .join('\n');

  return `export interface ${name} {\n${properties}\n}`;
}

function mapJsonSchemaTypeToTS(schema: any): string {
  if (schema.type === 'string') {
    if (schema.enum) return schema.enum.map(v => `'${v}'`).join(' | ');
    return 'string';
  }
  if (schema.type === 'number' || schema.type === 'integer') return 'number';
  if (schema.type === 'boolean') return 'boolean';
  if (schema.type === 'array') return `${mapJsonSchemaTypeToTS(schema.items)}[]`;
  if (schema.type === 'object') return 'Record<string, any>';
  return 'any';
}

Hint 3: Generate API Methods For each endpoint in paths:

function generateMethod(path: string, method: string, operation: any): string {
  const functionName = operation.operationId || generateOperationId(path, method);
  const params = extractParameters(operation);
  const requestBody = operation.requestBody;
  const response = operation.responses['200'] || operation.responses['201'];

  return `
  async ${functionName}(${params}): Promise<${getResponseType(response)}> {
    const response = await this.axios.${method}('${path}', ${getRequestConfig()});
    return ${getResponseSchema()}.parse(response.data);
  }
  `;
}

Hint 4: Add Authentication Generate authentication logic based on securitySchemes:

if (spec.components.securitySchemes.bearerAuth) {
  // Add Bearer token to headers
  headers['Authorization'] = `Bearer ${config.apiKey}`;
}

if (spec.components.securitySchemes.apiKey) {
  // Add API key to query or header
  const apiKeyLocation = spec.components.securitySchemes.apiKey.in;
  if (apiKeyLocation === 'header') {
    headers[spec.components.securitySchemes.apiKey.name] = config.apiKey;
  }
}

Books That Will Help

Topic Book Chapter
OpenAPI fundamentals “Designing Web APIs” by Brenda Jin et al. Ch. 3-4
Code generation “Code Generation in Action” by Jack Herrington Ch. 1-2
TypeScript types “Programming TypeScript” by Boris Cherny Ch. 6
REST API patterns “RESTful Web API Patterns” by Mike Amundsen Ch. 5

Common Pitfalls and Debugging

Problem 1: “Generated types don’t match runtime data”

  • Why: OpenAPI spec is out of sync with actual API responses
  • Fix: Add runtime validation with Zod to catch mismatches early
  • Quick test: Call real API and log response.data — does it match the schema?

Problem 2: “Circular references cause infinite loop”

  • Why: Schema A references Schema B, which references Schema A
  • Fix: Use lazy evaluation in Zod: z.lazy(() => UserSchema)
  • Quick test: Try generating types for User → Team → User — does it terminate?

Problem 3: “Generated code is unreadable”

  • Why: Long lines, no formatting, missing comments
  • Fix: Run generated code through Prettier: prettier --write src/**/*.ts
  • Quick test: Open generated file — would you be comfortable editing it?

Problem 4: “Authentication doesn’t work”

  • Why: Security scheme in spec doesn’t match actual API requirements
  • Fix: Test generated client against real API with curl equivalent
  • Quick test: await client.tasks.list() — does it return 401 or actual data?

Definition of Done

  • OpenAPI spec is parsed and validated
  • TypeScript interfaces are generated for all schemas
  • Zod validators are generated for runtime validation
  • API client class is generated with typed methods for each endpoint
  • Authentication is implemented based on securitySchemes
  • Error handling and request interceptors are included
  • Generated SDK can successfully call at least one endpoint
  • package.json, tsconfig.json, and README are generated
  • Generated code compiles without errors
  • Tests pass for basic SDK functionality