Skip to Content

@TypedRoute

@nestia/core
export namespace TypedRoute { export function Get(path?: string): MethodDecorator; export function Post(path?: string): MethodDecorator; export function Put(path?: string): MethodDecorator; export function Patch(path?: string): MethodDecorator; export function Delete(path?: string): MethodDecorator; }

Drop-in replacement for @Get, @Post, @Put, @Patch, @Delete from @nestjs/common. Behavior is identical plus:

  • The response value is type-checked at runtime against the return type.
  • JSON serialization is up to 200× faster than class-transformer.
  • The endpoint becomes visible to @nestia/sdk — it appears in the generated SDK, Swagger document, and e2e suite.

If a controller method returns something that doesn’t match its declared return type, the server throws 500 Internal Server Error instead of sending malformed JSON to the client. That alone catches a category of bugs you would otherwise find in production.


Basic usage

src/controllers/ArticlesController.ts
import { TypedRoute } from "@nestia/core"; import { Controller } from "@nestjs/common"; import { tags } from "typia"; interface IArticle { id: string & tags.Format<"uuid">; title: string; body: string; created_at: string & tags.Format<"date-time">; } @Controller("articles") export class ArticlesController { /** * Pick a random article. */ @TypedRoute.Get("random") public async random(): Promise<IArticle> { return { id: "2b5e21d8-0e44-4482-bd3e-4540dee7f3d6", title: "Hello Nestia", body: "Just use TypedRoute.Get() like this.", created_at: new Date().toISOString(), }; } }

That’s the entire change from vanilla NestJS — @Get("random") becomes @TypedRoute.Get("random"), and the return type is now enforced.


What gets validated

Everything the type expresses:

  • Field presence and primitive types.
  • tags.* constraints (Format, MinLength, Maximum, Pattern, etc.).
  • Nested objects and arrays.
  • Discriminated unions, recursive shapes, nullable / optional fields.

If validation passes, Nestia stringifies the value with a hand-generated encoder for the exact return type — no generic JSON.stringify recursion, no class-transformer reflection.

The build-time transformer reads that return type and emits the runtime validator and stringifier directly.


When to use plain @Get instead

Mixing is fine. Reach for the vanilla decorator when:

  • The method returns a Stream, a Buffer, or an HTTP redirect — Nestia’s stringifier expects JSON-serializable data.
  • The return type involves class instances whose toJSON() behavior you rely on (Nestia honors toJSON on Primitive<T>, but the contract is subtler — see the Primitive type in the generated SDK).

@nestia/sdk reads vanilla routes too: a @Get() method appears in the SDK, the Swagger document, and the e2e suite, typed from its declared return type. What it gives up is the runtime validation and serialization of the response. To leave a route out, tag the method @ignore, which removes it from every generator, or @internal / @hidden, which remove it from the Swagger document only.


EncryptedRoute

@nestia/core
export namespace EncryptedRoute { export function Get(path?: string): MethodDecorator; export function Post(path?: string): MethodDecorator; // ...same signatures as TypedRoute }

Same family, but the response body is AES-128/256 CBC encrypted with PKCS #5 padding and Base64 encoded, and the SDK decrypts it automatically.

Like @EncryptedBody, this is an obfuscation layer to run on top of TLS, not authenticated encryption. AES-CBC here carries no message authentication code, so treat it as a way to hide the payload from casual inspection or to satisfy a second-encryption-pass mandate, and keep HTTPS as the transport rather than relying on it as your only confidentiality or integrity control.

Encrypted routes answer with a text/plain response on Express and Fastify alike, because the wire carries ciphertext, and the Swagger document declares it so: the response is marked x-nestia-encrypted: true and its description starts with a warning. An error response stays JSON. Only the generated SDK (@nestia/sdk) can call them: it decrypts on the client side, so consumers see typed, decrypted data.

Encryption password

@EncryptedRoute and @EncryptedBody read the password of their controller class, which one of two decorators gives it:

@nestia/core
export function EncryptedModule( metadata: ModuleMetadata, password: IEncryptionPassword.Closure, ): ClassDecorator; export function EncryptedController( path: string, password: IEncryptionPassword | IEncryptionPassword.Closure, ): ClassDecorator;
  • EncryptedModule replaces @Module() and gives its password to every controller it reaches. That covers its own controllers and those of every import, however deep: module classes, dynamic modules ({ module, imports, controllers }), forwardRef()s, promises of them, and cyclic imports. EncryptedModule.dynamic(path, password) builds one from the controllers in a directory.
  • @EncryptedController(path, password) replaces @Controller() for one controller and gives it its own password. A subclass of it inherits that password.
  • The controller’s own password wins. Inside an EncryptedModule, an @EncryptedController keeps its own password, and only the controllers without one take the module’s.

A closure receives the request headers, the body, and the direction ("encode" or "decode"), so the password can depend on the caller. The generated SDK encrypts with connection.encryption, which must hold the password of the controller being called.


Configuration

No compilerOptions.plugins entry is needed for the default behavior. Without an explicit plugin config, @TypedRoute uses "assert" for response serialization and applies no LLM schema restriction.

Add a plugin entry only when you want to override those defaults:

tsconfig.json
{ "compilerOptions": { "strict": true, "plugins": [ { "transform": "typia/lib/transform", "enabled": false }, { "transform": "@nestia/core/native/transform.cjs", "stringify": "validate.log", "llm": { "strict": true } } ] } }

stringify — which serializer to use

The value picks the typia function TypedRoute calls. Each has slightly different runtime behavior:

ValueBehavior
"assert" (default)Validate; on failure throw; on success stringify.
"stringify"Skip validation; stringify directly. Fastest, least safe.
"is"Type-check; on failure throw 500; on success stringify.
"validate"Collect every type error before throwing.
"validate.log"Same as "validate", but on error log instead of throwing — useful during a migration.
nullUse plain JSON.stringify. No validation, no specialized encoder.

The same modes apply to @TypedQuery.Get() and its siblings, whose responses are query strings. Any other value fails the build with a diagnostic listing these values.

Configure the logger for "validate.log":

import { TypedRoute } from "@nestia/core"; TypedRoute.setValidateErrorLogger((err) => { console.error(err); // or ship it to your APM });

llm — restrict to LLM-friendly schemas

"llm": true // forbid tuples "llm": { "strict": true } // also forbid additionalProperties and optional fields

When set, the transformer rejects any TypedRoute whose return type falls outside the schema subset accepted by LLM function calling. Useful when you intend to expose endpoints to @nestia/chat or another agentic AI runtime — the check happens at build time, not at runtime.

See Swagger → Chat for the full LLM integration story.


Benchmarks

typia.json.stringify<T> is up to 200× faster than class-transformer. Treat that as the peak case; 30–100× is the typical real-server speedup.

@TypedRoute matters in production because JSON stringification is the one HTTP-pipeline step that runs synchronously on the main thread. Slow encoders block subsequent requests. The faster encoder is the kind of free win you would otherwise pay for by migrating to fastify.


See also

Last updated on