Skip to Content
📖 Guide Documents🚀 Tutorial3. Your First Endpoint

Your First Endpoint

In this chapter you write a controller from scratch — a tiny “article store” with two routes — and see exactly which decorators change.

If you cloned the starter from the previous chapter, open src/controllers/ and follow along. If not, the code is self-contained.


1. Define the data type

Create src/structures/IArticle.ts:

src/structures/IArticle.ts
import { tags } from "typia"; export interface IArticle { id: string & tags.Format<"uuid">; title: string & tags.MinLength<3> & tags.MaxLength<50>; body: string; created_at: string & tags.Format<"date-time">; } export namespace IArticle { export interface ICreate { title: string & tags.MinLength<3> & tags.MaxLength<50>; body: string; } }

That is the entire schema. Two TypeScript interfaces, no decorators, no separate file for OpenAPI, no separate file for validation. The tags.* intersection types are how Nestia attaches constraints — they have no runtime cost in your TypeScript code; they live purely in the type system.

Why an interface and not a class? Both work, but interfaces make it clearer that Nestia derives behavior from the type, not from a class with metadata. You will see this pattern throughout the docs.


2. Write the controller

Create src/controllers/ArticlesController.ts. Note the inline tags.Format<"uuid"> on the :id parameter — tags.* works the same way inline on a method parameter as it does on an interface field.

src/controllers/ArticlesController.ts
import { TypedBody, TypedParam, TypedRoute } from "@nestia/core"; import { Controller } from "@nestjs/common"; import { tags } from "typia"; import { IArticle } from "../structures/IArticle"; @Controller("articles") export class ArticlesController { /** * Create a new article. * * @param input The article payload. * @returns The stored article with `id` and `created_at` populated. */ @TypedRoute.Post() public async create( @TypedBody() input: IArticle.ICreate, ): Promise<IArticle> { return { ...input, id: "2b5e21d8-0e44-4482-bd3e-4540dee7f3d6", created_at: new Date().toISOString(), }; } /** * Read one article by ID. */ @TypedRoute.Get(":id") public async at( @TypedParam("id") id: string & tags.Format<"uuid">, ): Promise<IArticle> { return { id, title: "Hello Nestia", body: "Welcome to your first endpoint", created_at: new Date().toISOString(), }; } }

Register the controller in your application module — same as any other NestJS controller.


3. The three decorators you just used

Vanilla NestJSNestiaWhat changes
@Get(path)@TypedRoute.Get(path)Response is validated then serialized 200× faster.
@Post(path)@TypedRoute.Post(path)Same.
@Body()@TypedBody()Body is validated against the parameter type. 400 on mismatch.
@Param("id")@TypedParam("id")Param is coerced and validated to the declared type — uuid, int32, etc.
@Query("q")@TypedQuery()Query string is validated against the parameter type.

Mental model: every @Typed* decorator is the same as the vanilla one, with type-driven validation added at the boundary. You do not lose anything from NestJS — guards, interceptors, pipes, exception filters all still work.

Mixing is allowed. You can use @Get and @TypedRoute.Get in the same project. The Nestia generators only pick up @Typed* routes for SDK / Swagger / e2e, so use vanilla decorators wherever you don’t need validation or schema generation.


4. Try it

Restart the server (if start:dev is not already watching) and:

Terminal
# Happy path — 201 Created curl -X POST http://localhost:37001/articles \ -H "Content-Type: application/json" \ -d '{"title": "My first post", "body": "Hello!"}' # Bad payload — 400 Bad Request with a clear path curl -X POST http://localhost:37001/articles \ -H "Content-Type: application/json" \ -d '{"title": "no", "body": 42}'

The bad payload comes back with a structured error pointing at $input.title (too short) and $input.body (wrong type). You did not write a single line of validation code.


What just happened

The Nestia transformer ran at compile time. It looked at every @TypedBody() and @TypedRoute.*() it could see and inlined a hand-written validator for that exact type into the compiled JavaScript. There is no schema registry, no decorator metadata reflection, no runtime type inference.

That compile-time pass is why the runtime path has no schema registry, decorator metadata reflection, or runtime type inference.


Next

You have a typed endpoint. Next, Validate Inputs — adding format constraints, custom checks, and seeing how error messages are shaped.

Last updated on