|
| 1 | +# API Client Examples |
| 2 | + |
| 3 | +These are production-ready API client wrappers for your generated typed-openapi code. Copy the one that fits your needs and customize it. |
| 4 | + |
| 5 | +## Basic API Client (`api-client-example.ts`) |
| 6 | + |
| 7 | +A simple, dependency-free client that handles: |
| 8 | +- Path parameter replacement (`{id}` and `:id` formats) |
| 9 | +- Query parameter serialization (including arrays) |
| 10 | +- JSON request/response handling |
| 11 | +- Custom headers |
| 12 | +- Basic error handling |
| 13 | + |
| 14 | +### Setup |
| 15 | + |
| 16 | +1. Copy the file to your project |
| 17 | +2. Update the import path to your generated API file: |
| 18 | + ```typescript |
| 19 | + import { type EndpointParameters, type Fetcher, createApiClient } from './generated/api'; |
| 20 | + ``` |
| 21 | +3. Set your API base URL: |
| 22 | + ```typescript |
| 23 | + const API_BASE_URL = process.env['API_BASE_URL'] || 'https://your-api.com'; |
| 24 | + ``` |
| 25 | +4. Uncomment the client creation: |
| 26 | + ```typescript |
| 27 | + export const api = createApiClient(fetcher, API_BASE_URL); |
| 28 | + ``` |
| 29 | + |
| 30 | +### Usage |
| 31 | + |
| 32 | +```typescript |
| 33 | +// GET request with query params |
| 34 | +const users = await api.get('/users', { |
| 35 | + query: { page: 1, limit: 10, tags: ['admin', 'user'] } |
| 36 | +}); |
| 37 | + |
| 38 | +// POST request with body |
| 39 | +const newUser = await api.post('/users', { |
| 40 | + body: { name: 'John', email: 'john@example.com' } |
| 41 | +}); |
| 42 | + |
| 43 | +// With path parameters |
| 44 | +const user = await api.get('/users/{id}', { |
| 45 | + path: { id: '123' } |
| 46 | +}); |
| 47 | + |
| 48 | +// With custom headers |
| 49 | +const result = await api.get('/protected', { |
| 50 | + header: { Authorization: 'Bearer your-token' } |
| 51 | +}); |
| 52 | +``` |
| 53 | + |
| 54 | +## Validating API Client (`api-client-with-validation.ts`) |
| 55 | + |
| 56 | +Extends the basic client with schema validation for: |
| 57 | +- Request body validation before sending |
| 58 | +- Response validation after receiving |
| 59 | +- Type-safe validation error handling |
| 60 | + |
| 61 | +### Setup |
| 62 | + |
| 63 | +1. Follow the basic client setup steps above |
| 64 | +2. Import your validation library and schemas: |
| 65 | + ```typescript |
| 66 | + // For Zod |
| 67 | + import { z } from 'zod'; |
| 68 | + import { EndpointByMethod } from './generated/api'; |
| 69 | + |
| 70 | + // For Yup |
| 71 | + import * as yup from 'yup'; |
| 72 | + import { EndpointByMethod } from './generated/api'; |
| 73 | + ``` |
| 74 | +3. Implement the validation logic in the marked TODO sections |
| 75 | +4. Configure validation settings: |
| 76 | + ```typescript |
| 77 | + const VALIDATE_REQUESTS = true; // Validate request bodies |
| 78 | + const VALIDATE_RESPONSES = true; // Validate response data |
| 79 | + ``` |
| 80 | + |
| 81 | +### Validation Implementation Example (Zod) |
| 82 | + |
| 83 | +```typescript |
| 84 | +// Request validation |
| 85 | +if (VALIDATE_REQUESTS && params?.body) { |
| 86 | + const endpoint = EndpointByMethod[method as keyof typeof EndpointByMethod]; |
| 87 | + const pathSchema = endpoint?.[actualUrl as keyof typeof endpoint]; |
| 88 | + if (pathSchema?.body) { |
| 89 | + pathSchema.body.parse(params.body); // Throws if invalid |
| 90 | + } |
| 91 | +} |
| 92 | + |
| 93 | +// Response validation |
| 94 | +const responseData = await responseClone.json(); |
| 95 | +const endpoint = EndpointByMethod[method as keyof typeof EndpointByMethod]; |
| 96 | +const pathSchema = endpoint?.[actualUrl as keyof typeof endpoint]; |
| 97 | +const statusSchema = pathSchema?.responses?.[response.status]; |
| 98 | +if (statusSchema) { |
| 99 | + statusSchema.parse(responseData); // Throws if invalid |
| 100 | +} |
| 101 | +``` |
| 102 | + |
| 103 | +### Error Handling |
| 104 | + |
| 105 | +```typescript |
| 106 | +try { |
| 107 | + const result = await api.post('/users', { |
| 108 | + body: { name: 'John', email: 'invalid-email' } |
| 109 | + }); |
| 110 | +} catch (error) { |
| 111 | + if (error instanceof ValidationError) { |
| 112 | + if (error.type === 'request') { |
| 113 | + console.error('Invalid request data:', error.validationErrors); |
| 114 | + } else { |
| 115 | + console.error('Invalid response data:', error.validationErrors); |
| 116 | + } |
| 117 | + } else { |
| 118 | + console.error('Network or HTTP error:', error); |
| 119 | + } |
| 120 | +} |
| 121 | +``` |
| 122 | + |
| 123 | +## Customization Ideas |
| 124 | + |
| 125 | +- **Authentication**: Add token handling, refresh logic, or auth headers |
| 126 | +- **Retries**: Implement retry logic for failed requests |
| 127 | +- **Caching**: Add response caching with TTL |
| 128 | +- **Logging**: Add request/response logging for debugging |
| 129 | +- **Rate limiting**: Implement client-side rate limiting |
| 130 | +- **Metrics**: Add performance monitoring and error tracking |
| 131 | +- **Base URL per environment**: Different URLs for dev/staging/prod |
| 132 | + |
| 133 | +## Error Handling Enhancement |
| 134 | + |
| 135 | +You can enhance error handling by creating custom error classes: |
| 136 | + |
| 137 | +```typescript |
| 138 | +class ApiError extends Error { |
| 139 | + constructor( |
| 140 | + public readonly status: number, |
| 141 | + public readonly statusText: string, |
| 142 | + public readonly response: Response |
| 143 | + ) { |
| 144 | + super(`HTTP ${status}: ${statusText}`); |
| 145 | + this.name = 'ApiError'; |
| 146 | + } |
| 147 | +} |
| 148 | + |
| 149 | +// In your fetcher: |
| 150 | +if (!response.ok) { |
| 151 | + throw new ApiError(response.status, response.statusText, response); |
| 152 | +} |
| 153 | +``` |
0 commit comments