@taico/openapi-sdkgen
v0.3.3
Published
A TypeScript SDK generator from OpenAPI specs with focus on developer ergonomics and maintainability.
Readme
@taico/openapi-sdkgen
A TypeScript SDK generator from OpenAPI specs with focus on developer ergonomics and maintainability.
Install
npm install @taico/openapi-sdkgenCLI
openapi-sdkgen ./openapi.json ./generatedMotivation
The existing @taico/client package uses openapi-typescript-codegen with multiple post-processing scripts to work around limitations:
fix-esm-imports.js- Adds.jsextensions for ESM compatibilitymake-client-instantiable.js- Transforms static methods to accept per-instance config
This approach is fragile and hard to maintain. This package generates better SDKs from scratch instead.
Design Goals
- Instance-based configuration - No global singletons, auth per client instance
- Ergonomic API - Resource-based grouping, clean method signatures
- Type-safe - Full TypeScript support with strict mode
- Streaming support - SSE endpoints return AsyncIterable
- Simple generator - Transparent code generation, easy to debug
- No post-processing - Generate correct code from the start
Architecture
Three-Step Process
- Parse (
parser.ts) - Convert OpenAPI JSON to internal IR - Generate (
generator.ts) - Produce TypeScript source files - Emit (
index.ts) - Write files to disk
Generated Structure
generated/
├── index.ts # Main exports
├── client.ts # ApiClient main class
├── base-client.ts # BaseClient with fetch wrapper
├── types.ts # Type definitions from schemas
├── users-resource.ts # UsersResource class
├── tasks-resource.ts # TasksResource class
└── stream-resource.ts # StreamResource classUsage Pattern
const client = new ApiClient({
baseUrl: 'http://localhost:3000',
getAccessToken: async () => getToken(),
});
// Resource-based API
await client.users.getUser({ id: '123' });
await client.tasks.create({ body: { title: 'Task' } });
// Streaming
for await (const event of client.stream.events({ count: 10 })) {
console.log(event);
}
// AbortSignal support
const controller = new AbortController();
await client.users.list({ signal: controller.signal });Key Features
Instance-Based Auth
const client1 = new ApiClient({ baseUrl: 'http://api1.com', getAccessToken: () => token1 });
const client2 = new ApiClient({ baseUrl: 'http://api2.com', getAccessToken: () => token2 });No global state, no singleton issues.
Resource Grouping
Operations are grouped by OpenAPI tags into resource classes:
client.users.*- All user operationsclient.tasks.*- All task operationsclient.stream.*- All streaming operations
Streaming Support
SSE endpoints automatically detected and generated as async generators:
async *streamEvents(params): AsyncIterable<Event> {
yield* this.streamEvents('/stream/events', { params });
}Type Safety
- Request bodies typed from
requestBodyschema - Path/query params typed from
parameters - Response types from
responses[200].content - Optional fields handled correctly
- Enums generated as union types
Testing
The test suite is fully automated and self-contained under test/.
npm testThis runs the full pipeline:
- Clean - Remove previously generated client
- Generate spec - Start the test API in spec-generation mode, write
openapi.json, exit - Generate client - Run the generator against the spec
- Run tests - Start the test API server, run the test SDK against it, shut down
Test API (test/api/)
NestJS app with endpoints exercising OpenAPI features:
- GET with path/query params
- POST/PUT/PATCH with JSON body
- DELETE with no content response
- Nested objects and arrays
- Optional fields
- SSE streaming (
/stream/events) - Auth headers and custom header parameters
- Query parameters with special characters
The test API generates openapi.json via --generate-spec mode (starts, writes file, exits immediately — no server needed).
Test Client (test/client/)
test-sdk.ts imports the generated SDK and makes real HTTP calls to the test API, validating both types and runtime behavior including streaming and AbortSignal.
Tradeoffs
What We Gain
✅ No post-processing — generate correct code directly ✅ Cleaner code — no regex hacks on generated output ✅ Instance config — no global singleton ✅ Better DX — resource grouping, clear method names ✅ Streaming — first-class AsyncIterable support ✅ Maintainability — simple generator, easy to understand
What We Lose
❌ More code — custom generator vs off-the-shelf tool ❌ Edge cases — may not handle all OpenAPI features yet ❌ Battle-tested — new code vs mature library
Comparison to Current Approach
| Aspect | Current (openapi-typescript-codegen) | New Generator |
|--------|---------------------------------------|---------------|
| Post-processing | 2 scripts (fix-esm, make-instantiable) | None |
| Auth pattern | Global with per-call override | Instance-based |
| Resource grouping | Service classes | Resource classes |
| Streaming | Not supported | AsyncIterable |
| Maintainability | Fragile regex transforms | Transparent generation |
Limitations
Current implementation doesn't handle:
- Complex schema compositions (allOf, oneOf, anyOf)
- Custom media types beyond JSON
- File uploads
- Cookie parameters
- Complex security schemes
File Structure
packages/openapi-sdkgen/
├── src/
│ ├── types.ts # Internal IR types
│ ├── parser.ts # OpenAPI -> IR
│ ├── generator.ts # IR -> TypeScript
│ ├── index.ts # Main API
│ └── cli.ts # CLI tool
├── test/
│ ├── api/ # NestJS test API (spec source)
│ │ └── src/
│ │ ├── main.ts
│ │ ├── app.module.ts
│ │ ├── users.controller.ts
│ │ ├── tasks.controller.ts
│ │ └── stream.controller.ts
│ ├── client/ # Test SDK + generated output
│ │ ├── generated/ # Generated SDK (gitignored)
│ │ └── test-sdk.ts
│ └── run.mjs # Test orchestrator
├── package.json
├── tsconfig.json
└── README.md