Skip to Content
📖 Guide DocumentsS/W Development KitSDK Builder

SDK Generator

@nestia/sdk reads your NestJS controllers and emits a client SDK — one typed function per route, plus a copy of the DTO types and an IConnection config object. The result is a normal TypeScript module a frontend, mobile app, or test suite can import.

The SDK is hand-shaped TypeScript, not a fetch wrapper around generated JSON. Renaming a DTO field on the server and re-running npx nestia sdk is a compile error on the client — exactly what you want.


Generate

nestia.config.ts
import { INestiaConfig } from "@nestia/sdk"; import { NestFactory } from "@nestjs/core"; import { AppModule } from "./src/AppModule"; const config: INestiaConfig = { input: () => NestFactory.create(AppModule), output: "src/api", }; export default config;
Terminal
npx nestia init # one-time scaffold, if you don't have nestia.config.ts npx nestia sdk # generate the SDK

Output: src/api/ with:

src/api/ ├── HttpError.ts ├── IConnection.ts ├── Primitive.ts ├── module.ts └── functional/ └── articles/ └── index.ts # one file per controller, one function per route

Anatomy of a generated function

src/api/functional/articles/index.ts (excerpt)
/** * Create a new article. * * @param input The article payload. * @returns The stored article with `id` and `created_at` populated. * * @controller ArticlesController.create() * @path POST /articles */ export async function create( connection: IConnection, input: create.Input, ): Promise<create.Output> { /* ... */ } export namespace create { export type Input = Primitive<IArticle.ICreate>; export type Output = Primitive<IArticle>; // ... }
  • Function signature — derived from the controller method, including JSDoc.
  • Primitive<T> — the wire-form of T (methods stripped, Date becomes string, etc.).
  • IConnection — config object the client passes: host, optional headers, optional simulate.
  • Path parameters — each is URI-encoded into the URL, and a nullish one is sent as "null". A value of . or .. throws before any request is sent. The URL would resolve it as a dot segment and address another route, and no encoding avoids that.

Nothing in this file is hand-written.


Binary responses

When a route declares a binary response Content-Type with Nest’s @Header("Content-Type", "...") or Swagger’s @ApiProduces("..."), generated SDK functions return ReadableStream<Uint8Array<ArrayBufferLike>> instead of trying to parse the body as JSON or text.

A successful binary response with a null body returns an empty, closed stream. Consume the stream through its reader, cancel the reader if consumption stops early, and release its lock when finished.

With simulation enabled, a binary endpoint’s random() and simulated calls return empty, closed streams with the same stream type as real responses.

import { Controller, Get, Header, StreamableFile } from "@nestjs/common"; @Controller("files") export class FileController { @Header("Content-Type", "image/png") @Get("thumbnail") public thumbnail(): StreamableFile { return new StreamableFile(/* readable source */); } }

The generated Swagger response is emitted as type: "string" with format: "binary". Binary detection currently covers image/*, video/*, audio/*, application/octet-stream, and application/pdf.


INestiaConfig keys

KeyTypePurpose
input() => Promise<INestApplication> or { include: string[] }How to find your controllers.
outputstringWhere to write the SDK. Typical: "src/api".
distributestringStage a publishable npm package layout. See Distribution.
simulatebooleanEmbed a mock backend. See Mockup Simulator.
e2estringOutput directory for generated tests. See E2E.
swaggerISwaggerConfigEmit a swagger.json too. See Swagger.
propagatebooleanSDK returns a discriminated union instead of throwing. See Propagation Mode below.
clonebooleanCopy DTO types into the SDK’s own structures/ directory.
primitivebooleanWrap DTOs with Primitive<T> (default true).
assertbooleanValidate SDK call parameters at runtime via typia.assert.
jsonbooleanUse typia.assertStringify for 10× faster JSON serialization in the SDK.
keywordbooleanPass SDK function arguments as a single keyword object instead of positional (default false).

An SDK function’s parameters take the handler’s parameter names, which are the argument object’s keys when keyword is true. A destructured parameter, such as @TypedBody() { title }: IArticle.ICreate, declares no name, so the SDK names it after what it carries: its field in camel case (@Query("page-size") becomes pageSize), otherwise its kind (body, query, headers), with a _ prefix when another parameter of the handler already has that name.


How input finds controllers

Two shapes:

// 1. NestFactory.create (recommended) input: () => NestFactory.create(AppModule), // 2. Glob (no Nest bootstrapping) input: { include: ["src/**/*.controller.ts"] },

Use the factory form when your app needs app.setGlobalPrefix() or app.enableVersioning() — those calls run inside the factory and the generator picks up their effects. Use the glob form for projects where bootstrapping is expensive or impossible (no DB connection available at build time).

Apply prefixes inside the factory:

input: async () => { const app = await NestFactory.create(AppModule); app.setGlobalPrefix("api"); app.enableVersioning({ type: VersioningType.URI, prefix: "v" }); return app; },

The generated SDK paths reflect the prefix and version automatically.


Propagation mode

Default behavior: an SDK call throws HttpError on any non-2xx response.

propagate: true swaps that for a discriminated union:

type Output = IPropagation< { 200: IArticle; 400: TypeGuardError.IProps; }, 200 // success branch >; const out = await api.functional.articles.create(connection, input); if (out.success) out.data.id; // IArticle (200) else if (out.status === 400) out.data.expected; // TypeGuardError.IProps (400) else out.data; // unknown — outside declared statuses

The map of status codes comes from @TypedRoute.* (success) plus every @TypedException() on the route. Range matchers ("4XX", "5XX") work the same way.


Clone mode

clone: true makes the SDK self-contained — every DTO type used in any route gets copied into the SDK’s own structures/ directory, breaking the dependency on the server’s source.

Cloned properties preserve ? when the server enables exactOptionalPropertyTypes: optional?: boolean stays optional?: boolean. Explicit undefined unions are retained. As with JSON serialization, properties whose values can be undefined may be omitted in the cloned type.

Type tags are copied as written: number & tags.Minimum<3> stays number & tags.Minimum<3>. A tag of your own, even one named like a typia tag, is copied as the equivalent tags.TagBase<{ ... }>, and a JSDoc comment tag such as @format uri becomes the type tag it names, tags.Format<"uri">.

Why you’d want this:

  • Your DTOs come from an ORM (Prisma Article, TypeORM entity). Without clone, consumers must install @prisma/client to compile the SDK.
  • You publish the SDK to npm and don’t want consumers pulling your full server tree as a transitive dependency.

Trade-off: regenerating the SDK refreshes the cloned types, but if you hand-edit the SDK after generation those edits are lost on the next npx nestia sdk run. Don’t.


Distribute

To publish the SDK as an npm package:

nestia.config.ts
const config: INestiaConfig = { input: () => NestFactory.create(AppModule), output: "src/api", distribute: "packages/api", };

After npx nestia sdk, packages/api/ is a publishable npm package layout. See Distribution for the customization steps.

For workspace-internal distribution (monorepo), use pnpm workspaces instead of publishing — see PNPM Monorepo.


CLI flags

Terminal
npx nestia sdk # uses nestia.config.ts + tsconfig.json in cwd npx nestia sdk --config nestia2.config.ts # alternate config npx nestia sdk --project tsconfig2.json # alternate tsconfig

Useful when one repo has two NestJS apps (e.g. a monorepo with apps/admin and apps/public).


See also

Last updated on