CLI

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 ./client

Flags

FlagDescriptionDefault
--name <name>Generate SDK for a specific named OpenAPI outputall outputs
--output <path>Override output directory for generated SDKfrom config
--config <path>Path to tsgonest config fileauto-detect

Config

Configure SDK generation per OpenAPI output:

tsgonest.config.ts
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:

tsgonest.config.ts
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 methods

Each 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

PropertyTypeDescription
baseUrlstringBase URL for all requests
headersRecord<string, string> | () => Record<string, string>Static or dynamic headers (e.g., auth tokens)
fetcher(url, init) => Promise<Response>Custom fetch implementation (default: fetch)
timeoutnumberRequest timeout in milliseconds
throwOnErrorbooleanThrow on non-2xx responses instead of returning { error }
onRequest(url, init) => RequestInitHook to modify requests before sending
onResponse(response, context) => Response | voidHook called on every response
onError(error, context) => SDKError | voidHook 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.json

The 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.

On this page