AponiaJSDocs
Essentials

Controllers and routes

Map the seven HTTP method decorators to controller handlers and joined route paths.

Controllers own HTTP routes and delegate application behavior to injected services.

import { Controller, Get, Post } from "@aponiajs/common";

@Controller("greetings")
export class GreetingController {
  @Get()
  list(): string[] {
    return ["Hello"];
  }

  @Post("refresh")
  refresh(): string {
    return "Refreshed";
  }
}

Paths are normalized and joined:

ControllerMethodFinal route
@Controller("greetings")@Get()/greetings
@Controller("/greetings/")@Post("/refresh/")/greetings/refresh
@Controller()@Get()/

Leading and trailing slashes are trimmed from both segments before they are joined, so "greetings" and "/greetings/" are equivalent.

Supported methods

The implemented decorators are:

  • @Get(path?)
  • @Post(path?)
  • @Put(path?)
  • @Patch(path?)
  • @Delete(path?)
  • @Head(path?)
  • @Options(path?)

Each decorator has three call signatures:

@Get()                       // path defaults to ""
@Get(":id")                  // path only
@Get(":id", findUserSchema)  // path and route schema
@Get(findUserSchema)         // route schema only

The schema form is described in route validation.

Handler arguments

A handler declares what it needs with request parameter decorators:

import { Body, Controller, Get, Param, Post, Query } from "@aponiajs/common";

@Controller("users")
export class UserController {
  @Get(":id")
  findOne(@Param("id") id: string, @Query("expand") expand: string | undefined) {
    return { id, expand };
  }

  @Post()
  create(@Body() body: { name: string }) {
    return body;
  }
}

A handler declared with no parameter decorators may declare one unannotated parameter to receive the whole request context:

import { Controller, Get } from "@aponiajs/common";
import { type ElysiaRouteContext } from "@aponiajs/platform-elysia";

@Controller("health")
export class HealthController {
  @Get()
  read(context: ElysiaRouteContext) {
    return { path: context.path };
  }
}

Types come from your annotations

TypeScript cannot contextually type a decorated method's parameters, so the types are whatever the handler declares — exactly as in NestJS. Keep a schema in a const and derive the type from it with z.infer or Static<typeof …> rather than expecting inference from the decorator.

Controller construction

Controllers are instantiated once per owning module. Constructor dependencies follow the same visibility rules as providers.

@Controller("greetings")
export class GreetingController {
  constructor(private readonly greetings: GreetingService) {}

  @Get()
  getGreeting(): string {
    return this.greetings.createGreeting();
  }
}

Decorated routes are compiled into immutable plans and registered directly on the root Elysia application during bootstrap. Parameter binding becomes a fixed-arity invoker instead of request-time metadata traversal. A controller missing @Controller() fails with INVALID_CONTROLLER before the server listens.

For Elysia hooks, macros, or a route that needs Elysia's complete handler signature, use native Elysia access or defineElysiaController().

On this page