tsgonest sdk
Generate a fully-typed TypeScript SDK client from your OpenAPI spec.
tsgonest sdk reads your generated OpenAPI document and produces a zero-dependency, fully-typed TypeScript SDK that mirrors your NestJS controllers. Each controller becomes a class with typed methods for every route.
Usage
# Generate SDK for all outputs that have sdk.output configured
tsgonest sdk
# Generate SDK for a specific named output
tsgonest sdk --name public
# Override output directory
tsgonest sdk --name public --output ./clientFlags
| Flag | Description | Default |
|---|---|---|
--name <name> | Generate SDK for a specific named OpenAPI output | all outputs |
--output <path> | Override output directory for generated SDK | from config |
--config <path> | Path to tsgonest config file | auto-detect |
Config
Configure SDK generation per OpenAPI output:
import { defineConfig } from '@tsgonest/runtime';
export default defineConfig({
openapi: [
{
name: 'public',
output: 'dist/public-api.json',
title: 'Public API',
sdk: { output: './sdk/public' },
},
{
name: 'internal',
output: 'dist/internal-api.json',
title: 'Internal API',
// No sdk — no SDK generated for this output
},
],
});Single-output configs work the same way:
import { defineConfig } from '@tsgonest/runtime';
export default defineConfig({
openapi: {
output: 'dist/openapi.json',
title: 'My API',
version: '1.0.0',
sdk: { output: './sdk' },
},
});During tsgonest build, per-output SDKs are generated automatically.
Generated Output
sdk/
client.ts # Core HTTP client, types (SDKResult, ClientConfig, etc.)
types.ts # All shared TypeScript types from OpenAPI schemas
sse.ts # SSE connection helper (tree-shakeable)
form-data.ts # FormData builder helper (tree-shakeable)
index.ts # Barrel export
users/
index.ts # UsersController methods
orders/
index.ts # OrdersController methodsEach controller gets its own directory for clean imports and tree-shaking.
Client Setup
import { createClient } from './sdk';
const api = createClient({
baseUrl: 'http://localhost:3000',
});
// Fully typed — params, query, body, and response
const { data, error } = await api.users.findOne({ params: { id: '123' } });
if (error) {
console.error(error.message);
} else {
console.log(data.name); // typed as UserDto
}ClientConfig
| Property | Type | Description |
|---|---|---|
baseUrl | string | Base URL for all requests |
headers | Record<string, string> | () => Record<string, string> | Static or dynamic headers (e.g., auth tokens) |
fetcher | (url, init) => Promise<Response> | Custom fetch implementation (default: fetch) |
timeout | number | Request timeout in milliseconds |
throwOnError | boolean | Throw on non-2xx responses instead of returning { error } |
onRequest | (url, init) => RequestInit | Hook to modify requests before sending |
onResponse | (response, context) => Response | void | Hook called on every response |
onError | (error, context) => SDKError | void | Hook called on error responses |
Result Type
Every SDK method returns SDKResult<T>:
type SDKResult<T> =
| { data: T; error: null; response: Response }
| { data: null; error: SDKError; response: Response };This pattern provides type-safe error handling without try/catch:
const result = await api.orders.create({ body: { item: 'widget', quantity: 3 } });
if (result.error) {
// result.error is SDKError { status, message, body? }
console.error(`${result.error.status}: ${result.error.message}`);
} else {
// result.data is typed as OrderDto
console.log(result.data.id);
}Or with throwOnError: true:
const api = createClient({ baseUrl: '...', throwOnError: true });
try {
const { data } = await api.orders.findOne({ params: { id: '1' } });
console.log(data.id); // typed
} catch (err) {
// err is SDKError
}Features
Path, Query, and Header Parameters
Parameters are fully typed based on your controller signatures:
// GET /users/:id — path params
await api.users.findOne({ params: { id: '123' } });
// GET /users?page=1&limit=10 — query params
await api.users.findAll({ query: { page: 1, limit: 10 } });
// Custom headers
await api.users.findAll({ headers: { 'x-api-key': 'abc' } });Request Bodies
// POST /users — JSON body
await api.users.create({
body: { name: 'Alice', email: 'alice@example.com' },
});SSE (Server-Sent Events)
SSE endpoints return a typed SSEConnection:
const { data: response } = await api.orders.events({ params: { id: '123' } });
const sse = new SSEConnection(response);
for await (const event of sse) {
// event.event is typed ('created' | 'shipped' | 'cancelled')
// event.data is typed per event name
console.log(event.event, event.data);
}FormData / File Uploads
Multipart endpoints use the generated buildFormData helper:
await api.uploads.create({
body: buildFormData({ file: myFile, description: 'Photo' }),
});Abort Signals
const controller = new AbortController();
await api.users.findAll({
query: { page: 1 },
signal: controller.signal,
});
// Cancel the request
controller.abort();API Versioning
If your API uses URI versioning, the SDK groups controllers by version:
// Versioned API
await api.v1.users.findAll();
await api.v2.users.findAll();The globalPrefix and versionPrefix from your tsgonest config are automatically stripped from paths so the SDK method signatures stay clean.
Workflow
SDK generation happens automatically during tsgonest build when sdk.output is configured. For standalone generation:
tsgonest build # Compiles + generates openapi.json + SDK
tsgonest sdk # Regenerate SDK from existing openapi.jsonThe SDK is plain TypeScript with no runtime dependencies — copy it into a frontend project, publish it as an internal package, or commit it to a monorepo.