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:
- Should
rolebe a TypeScript enum or a union type ('admin' | 'member' | 'guest')? - How do you represent
oneOfin TypeScript? (Union type? Discriminated union?) - The
profilefield depends onrole— can you enforce this at the type level? - Should you generate separate types for AdminProfile and MemberProfile or inline them?
- How would Zod validate the
oneOfrelationship 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:
- “How would you handle versioning in a generated SDK? (e.g., API v1 vs v2)”
- “Your OpenAPI spec has a circular reference. How do you generate types without infinite loops?”
- “What’s the difference between compile-time type safety and runtime validation? Why do you need both?”
- “How would you handle authentication in a generated client? (API keys, OAuth2, etc.)”
- “The API spec changes frequently. How do you keep the SDK in sync without manual work?”
- “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
curlequivalent - 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