tsgonest vs NestJS CLI
How tsgonest compares to the standard NestJS CLI and its ecosystem of runtime libraries.
A typical NestJS project uses @nestjs/cli for building, class-validator + class-transformer for validation, and @nestjs/swagger for OpenAPI. Here's how tsgonest replaces each piece.
At a glance
| Feature | NestJS CLI + ecosystem | tsgonest |
|---|---|---|
| Compiler | tsc (TypeScript) | tsgo (Go, ~10x faster raw compilation) |
| Watch mode | nest start --watch (tsc --watch + nodemon) | tsgonest dev (native watcher) |
| Validation | class-validator decorators | Generated from types |
| Serialization | class-transformer + ClassSerializerInterceptor | Generated fast JSON serializers |
| OpenAPI | @nestjs/swagger + decorators | Static analysis, zero decorators |
| Type safety | Decorators can diverge from types | Types are the single source of truth |
| Runtime dependencies | 5+ packages | 1 lightweight runtime package |
| Build output | JavaScript only | JS + companions + OpenAPI |
Build speed
tsgonest uses Microsoft's typescript-go (tsgo) — a native Go port of the TypeScript compiler. Raw tsgo compilation is roughly 10x faster than tsc. The full tsgonest pipeline (compile + companion generation + OpenAPI) is about ~4x faster than tsc alone — while doing significantly more work.
Measured benchmark (realworld fixture, 7 controllers, 41 routes):
| Tool | Time | What it does |
|---|---|---|
tsc | 559ms | Compile only |
tsgonest build | 128ms | Compile + companions + OpenAPI |
| Project size | nest build (tsc) | tsgonest build (tsgo) |
|---|---|---|
| Small (~50 files) | ~2s | ~0.3s |
| Medium (~200 files) | ~8s | ~1s |
| Large (120 controllers, 838 routes) | ~60s+ | ~19s (cold), ~1.1s (cached) |
tsgonest also caches the post-processing step (companion generation, OpenAPI). If your source files haven't changed, only the tsgo emit runs.
Validation comparison
NestJS CLI approach
With the standard NestJS stack, validation requires multiple packages and extensive decorator usage:
npm install class-validator class-transformerimport { IsString, IsEmail, IsNumber, Min, Max, MinLength, MaxLength } from 'class-validator';
import { Transform } from 'class-transformer';
export class CreateUserDto {
@IsString()
@MinLength(1)
@MaxLength(255)
@Transform(({ value }) => value?.trim())
name: string;
@IsEmail()
email: string;
@IsNumber()
@Min(0)
@Max(150)
age: number;
}import { ValidationPipe } from '@nestjs/common';
app.useGlobalPipes(new ValidationPipe({ transform: true, whitelist: true }));Problems with this approach:
- Every property needs both a TypeScript type and decorators — they can diverge
class-validatorvalidates at runtime usingreflect-metadataclass-transformeris needed for transformation (trim, type coercion)- DTOs must be classes (not interfaces or type aliases)
- Performance overhead from runtime reflection
tsgonest approach
import { Min, Max, Email, Trim } from '@tsgonest/types';
export interface CreateUserDto {
name: string & Trim & Min<1> & Max<255>;
email: string & Email;
age: number & Min<0> & Max<150>;
}Validation is injected at compile time — no runtime setup needed.
Advantages:
- Types are the source of truth — no duplication
- Works with interfaces and type aliases — not just classes
- Validators are generated at build time — no runtime reflection
- Zero-runtime type annotations —
@tsgonest/typesproduces no JS - Transforms (trim, toLowerCase) are built into the generated code
Side-by-side comparison
// ❌ NestJS CLI: decorator soup, type duplication
export class CreateUserDto {
@IsString()
@MinLength(1)
@MaxLength(255)
@Transform(({ value }) => value?.trim())
name: string;
@IsEmail()
@Transform(({ value }) => value?.toLowerCase())
email: string;
@IsNumber()
@Min(0)
@Max(150)
age: number;
@IsOptional()
@IsString()
@Matches(/^https?:\/\//)
website?: string;
}// ✅ tsgonest: types express everything
import { Min, Max, Email, Trim, ToLowerCase, Pattern } from '@tsgonest/types';
export interface CreateUserDto {
name: string & Trim & Min<1> & Max<255>;
email: string & ToLowerCase & Email;
age: number & Min<0> & Max<150>;
website?: string & Pattern<"^https?:\\/\\/">;
}OpenAPI comparison
NestJS CLI approach
With @nestjs/swagger, every DTO property needs additional decorators:
npm install @nestjs/swagger swagger-ui-expressimport { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class CreateUserDto {
@ApiProperty({ description: 'User name', minLength: 1, maxLength: 255 })
@IsString()
@MinLength(1)
@MaxLength(255)
name: string;
@ApiProperty({ description: 'Email address', format: 'email' })
@IsEmail()
email: string;
@ApiProperty({ description: 'Age', minimum: 0, maximum: 150 })
@IsNumber()
@Min(0)
@Max(150)
age: number;
@ApiPropertyOptional({ description: 'Website URL' })
@IsOptional()
@IsString()
website?: string;
}import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
@ApiTags('users')
@ApiBearerAuth()
@Controller('users')
export class UserController {
@Post()
@ApiOperation({ summary: 'Create a user' })
@ApiResponse({ status: 201, type: UserResponse })
create(@Body() dto: CreateUserDto): UserResponse {
// ...
}
}import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
const config = new DocumentBuilder()
.setTitle('My API')
.setVersion('1.0')
.addBearerAuth()
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api-docs', app, document);This approach has significant drawbacks:
- Triple declaration: Each property is declared as a TS type, a class-validator decorator, and an
@ApiProperty - Runtime overhead: Document is generated at startup via reflection
- Startup time:
SwaggerModule.createDocumentscans all controllers at boot - Drift risk: Swagger decorators can easily get out of sync with validators
tsgonest approach
import { Min, Max, Email, Trim } from '@tsgonest/types';
export interface CreateUserDto {
name: string & Trim & Min<1> & Max<255>;
email: string & Email;
age: number & Min<0> & Max<150>;
website?: string;
}@Controller('users')
export class UserController {
@Post()
create(@Body() dto: CreateUserDto): UserResponse {
// That's it. No swagger decorators needed.
}
}import { defineConfig } from '@tsgonest/runtime';
export default defineConfig({
openapi: {
output: 'dist/openapi.json',
title: 'My API',
version: '1.0',
},
});The OpenAPI document is generated at build time via static analysis:
- Zero runtime cost
- Constraints from types are automatically reflected in the schema
- No additional decorators needed
- Tags derived from controller names
- Operation IDs from method names
- Route params, query params, and request bodies inferred from decorator types
Serialization comparison
NestJS CLI approach
import { Exclude, Expose, Type } from 'class-transformer';
export class UserResponse {
@Expose()
id: string;
@Expose()
name: string;
@Expose()
email: string;
@Exclude()
password: string; // hidden from responses
@Expose()
@Type(() => Date)
createdAt: Date;
}app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)));This uses reflect-metadata at runtime and JSON.stringify for serialization — the slowest possible path.
tsgonest approach
export interface UserResponse {
id: string;
name: string;
email: string;
createdAt: string;
}Serialization is injected at compile time — return values are automatically wrapped with transformTypeName().
The generated serializer uses string concatenation with known property names — ~1.4x faster than JSON.stringify for simple DTOs (benchmarked: 13.2M ops/s vs 9.8M ops/s). For complex nested objects the performance is comparable to JSON.stringify. Only declared properties are included in the output (no @Exclude needed — if it's not in the type, it's not serialized).
Validation performance is where the biggest gains are: tsgonest's generated validators are 25-84x faster than class-validator (benchmarked: 16.8M ops/s vs 345K ops/s for a simple DTO).
Dependencies
NestJS CLI ecosystem
{
"dependencies": {
"class-validator": "^0.14",
"class-transformer": "^0.5",
"@nestjs/swagger": "^7.0",
"swagger-ui-express": "^5.0",
"reflect-metadata": "^0.2"
}
}5+ additional runtime dependencies, each with their own transitive dependencies.
tsgonest
{
"dependencies": {
"tsgonest": "^0.1"
}
}One package. @tsgonest/runtime and @tsgonest/types are included as dependencies of tsgonest.
Migration path
Migrating from the NestJS CLI ecosystem to tsgonest can be done incrementally:
Step 1: Replace the build
"scripts": {
- "build": "nest build",
- "start:dev": "nest start --watch",
+ "build": "tsgonest build",
+ "start:dev": "tsgonest dev",
"start:prod": "node dist/main.js"
}Step 2: Add tsgonest config
import { defineConfig } from '@tsgonest/runtime';
export default defineConfig({
controllers: { include: ['src/**/*.controller.ts'] },
transforms: { validation: true, serialization: true },
openapi: { output: 'dist/openapi.json' },
});Step 3: Convert DTOs gradually
You can convert DTOs one at a time. tsgonest's compile-time injection only activates for types that have companion files, so your existing class-validator decorators continue working alongside tsgonest-generated validators.
// Before: class-validator
export class CreateUserDto {
@IsString()
@MinLength(1)
name: string;
}
// After: tsgonest
export interface CreateUserDto {
name: string & Min<1>;
}Step 4: Remove old pipes and dependencies
Remove ValidationPipe from your main.ts — tsgonest injects validation at compile time, so no runtime pipe is needed.
Step 5: Remove old dependencies
npm uninstall class-validator class-transformer @nestjs/swagger swagger-ui-express