Skip to main content
Controllers group related route handlers under a common URL prefix. In nestrs, a controller is a plain Rust struct annotated with #[controller]. Route handlers live in an impl block annotated with #[routes]. When the module builds, nestrs registers every handler in that impl block as an Axum route under the controller’s prefix.

Defining a controller

use nestrs::prelude::*;
use std::sync::Arc;

#[controller(prefix = "/api")]
pub struct AppController;
The prefix argument sets the base path for every route in the controller. Paths are joined with each handler’s path, so prefix = "/api" + #[get("/")] produces GET /api/.

Optional attributes

AttributeDescription
prefix = "..."Base path prepended to all route paths in this controller
version = "v1"URI version segment (requires enable_uri_versioning on NestApplication)
Use the #[version] attribute separately to apply a version to an entire controller struct:
#[version("v2")]
#[controller(prefix = "/api")]
pub struct AppControllerV2;

Registering routes with #[routes]

The #[routes] macro annotates an impl block and takes a state argument that names the injected service type. nestrs uses this to build an Axum router with the correct State type:
#[routes(state = AppService)]
impl AppController {
    #[get("/")]
    pub async fn root(State(service): State<Arc<AppService>>) -> &'static str {
        service.get_hello()
    }
}
state must be a type registered in the same module’s providers list. nestrs resolves it from the ProviderRegistry and injects it as Axum State.

HTTP method macros

nestrs provides a macro for every HTTP method:
MacroMethod
#[get("path")]GET
#[post("path")]POST
#[put("path")]PUT
#[patch("path")]PATCH
#[delete("path")]DELETE
#[options("path")]OPTIONS
#[head("path")]HEAD
#[all("path")]All methods
Each macro takes the route path as its argument. The path is appended to the controller’s prefix.

Route examples

Basic GET and POST

#[routes(state = AppService)]
impl AppController {
    #[get("/")]
    pub async fn root(State(service): State<Arc<AppService>>) -> &'static str {
        service.get_hello()
    }

    #[post("/users")]
    pub async fn create_user(
        State(service): State<Arc<AppService>>,
        ValidatedBody(dto): ValidatedBody<CreateUserDto>,
    ) -> Result<Json<UserResponse>, HttpException> {
        Ok(Json(service.create_user(dto)))
    }
}

Path parameters

Use Axum’s Path extractor. Parameters are declared in the route path with a colon prefix (:id):
#[get("/users/:id")]
pub async fn get_user(
    State(service): State<Arc<UserService>>,
    Path(id): Path<i64>,
) -> Result<Json<UserRow>, HttpException> {
    service.find_by_id(id).await
        .map(Json)
        .map_err(NotFoundException::new)
}

Query parameters

Use Axum’s Query extractor with a deserializable struct:
#[derive(serde::Deserialize)]
pub struct SearchQuery {
    pub q: String,
    pub limit: Option<usize>,
}

#[get("/search")]
pub async fn search(
    State(service): State<Arc<SearchService>>,
    Query(params): Query<SearchQuery>,
) -> Json<Vec<SearchResult>> {
    Json(service.search(&params.q, params.limit.unwrap_or(10)).await)
}

JSON request bodies

Use Axum’s Json extractor for unvalidated JSON, or ValidatedBody to run validator constraints automatically:
#[post("/users")]
pub async fn create_user(
    State(service): State<Arc<AppService>>,
    ValidatedBody(dto): ValidatedBody<CreateUserDto>,
) -> Result<Json<UserResponse>, HttpException> {
    if dto.name.eq_ignore_ascii_case("admin") {
        return Err(ConflictException::new("`admin` is reserved in this demo"));
    }
    Ok(Json(service.create_user(dto)))
}

Response customization

Custom HTTP status code

#[get("/created-style")]
#[http_code(201)]
pub async fn created_style() -> &'static str {
    "created-style"
}

Custom response headers

#[get("/header-style")]
#[response_header("x-powered-by", "nestrs")]
pub async fn header_style() -> &'static str {
    "header-style"
}

Redirects

#[get("/docs")]
#[redirect("https://docs.nestjs.com")]
pub async fn docs() -> &'static str {
    "docs"
}

Versioning a single route

You can version a single route within a controller using #[ver] while leaving other routes at the controller’s default version:
#[get("/feature")]
#[ver("v2")]
pub async fn versioned_feature() -> &'static str {
    "feature-route-v2"
}

Registering the controller in a module

Controllers must be listed in the controllers field of their module:
#[module(
    imports  = [DataModule],
    controllers = [AppController, AppControllerV2],
    providers  = [AppService],
)]
pub struct AppModule;
A single controller can only be registered in one module. Shared logic belongs in a provider (service), not a controller.