Rules & Limitations
What tsgonest can and cannot analyze at compile time, and how to work around the boundaries.
tsgonest is a compile-time tool. It uses static analysis of your TypeScript source code to generate validators, serializers, and OpenAPI schemas. It does not run your application, cannot evaluate runtime expressions, cannot look inside factory functions, and cannot resolve dynamic values. This page documents the boundaries and the escape hatches available to you.
Static Analysis Only
tsgonest reads your source code at build time. It does not start your NestJS application, instantiate any classes, or execute any of your code.
This means it cannot:
- Resolve runtime-computed values — variables, function return values, environment variables
- See through factory functions that return NestJS decorators
- Analyze dynamically constructed controller classes
- Evaluate conditional logic that determines types or routes at runtime
If tsgonest can't see it in the source text, it doesn't exist.
Factory Decorators Are Opaque
tsgonest introspects a fixed set of core NestJS decorators:
- Parameter decorators:
@Body,@Query,@Param,@Headers - Route decorators:
@Get,@Post,@Put,@Delete,@Patch,@Sse,@EventStream - Class/method decorators:
@Controller,@HttpCode,@Version
If you wrap any of these in a factory function, tsgonest cannot detect them:
// This breaks — tsgonest sees `@ValidatedBody()`, not `@Body()`
function ValidatedBody() {
return Body();
}
@Post()
create(@ValidatedBody() dto: CreateUserDto) { ... }Import aliases ARE supported:
import { Body as NestBody } from '@nestjs/common';
@Post()
create(@NestBody() dto: CreateUserDto) { ... } // worksCustom parameter decorators are also supported if you annotate them with @in JSDoc to tell tsgonest where the parameter comes from. See the parameters documentation for details.
Dynamic Paths Are Not Supported
Controller paths and route paths must be string literals or arrays of string literals.
// works
@Controller('users')
// works
@Controller(['v1/users', 'v2/users'])
// skipped with a warning — tsgonest cannot evaluate variables
const prefix = 'users';
@Controller(prefix)
// skipped with a warning
@Get(computedPath)When tsgonest encounters a dynamic path, it emits a warning and skips that controller or route entirely. No OpenAPI entry is generated for it.
Controller Classes Must Be Top-Level
Controllers must be declared at the top level of a module. Classes declared inside factory functions, conditionally defined, or dynamically generated are skipped:
// works
@Controller('users')
export class UsersController { ... }
// skipped — not a top-level declaration
function createController(prefix: string) {
@Controller(prefix)
class DynamicController { ... }
return DynamicController;
}This is intentional. An OpenAPI document is a static contract — it describes a fixed set of endpoints. If your controller's existence depends on runtime conditions, it cannot be represented in a static schema.
Generic Types in OpenAPI
OpenAPI 3.2 has no concept of generics. tsgonest flattens every generic instantiation into a concrete schema. PaginatedResponse<UserDto> becomes PaginatedResponse_UserDto with all properties materialized. Flat schemas work with every OpenAPI tool (Swagger UI, Redocly, SDK generators).
If a generic type argument can't be named (e.g., Wrapper<{ x: number }>), the schema is inlined and a warning suggests creating a named type alias.
Pre-registered type aliases win. If you write:
type ProductResponse = PaginatedResponse<Product>;tsgonest uses ProductResponse as the schema name, not PaginatedResponse_Product. Your chosen name takes priority.
@tsgonest-ignore
You can suppress companion file generation for specific types with the @tsgonest-ignore JSDoc tag:
/** @tsgonest-ignore */
interface InternalConfig {
dbHost: string;
dbPort: number;
// no .tsgonest.js companion generated for this type
}This is useful for types that are only used internally and don't need validation, serialization, or OpenAPI schema generation.
@Res() Routes Skip Serialization
If a route handler uses @Res() or @Response(), tsgonest does not inject serialization. You are manually controlling the response — tsgonest respects that.
@Get(':id')
@Returns<UserDto>() // used for OpenAPI only — no runtime serialization
async findOne(@Param('id') id: string, @Res() res: Response) {
const user = await this.usersService.findOne(id);
res.json(user); // you handle serialization
}@Returns<T>() on @Res() routes is purely for OpenAPI documentation. It tells tsgonest what the response schema looks like, but generates no runtime code for that route's return value.
What IS Supported
tsgonest handles the full breadth of TypeScript's type system at compile time:
- All TypeScript types: interfaces, type aliases, classes, enums, unions, intersections, tuples, mapped types, template literal types, recursive types
- JSDoc tags AND branded phantom types from
@tsgonest/types— can be mixed freely on the same type - Standard NestJS decorators with full import alias support via symbol resolution
- Custom parameter decorators annotated with
@inJSDoc - Generic type instantiation with automatic composite naming (
PaginatedResponse_UserDto) and type alias priority - Incremental builds and post-processing cache — unchanged files are not re-analyzed
- Array paths on controllers and routes
@HttpCode,@Version,@Sse, and@EventStreamdecorator extraction for OpenAPI