Skip to content

Controllers

Handling incoming requests and returning responses in NestForge.

Controllers are responsible for handling incoming requests and returning responses to the client.

A controller’s purpose is to receive specific requests for the application. The routing mechanism controls which controller receives which requests. Frequently, each controller has more than one route, and different routes can perform different actions.

In order to create a basic controller, we use the #[controller] macro. We’ll specify an optional path prefix in the #[controller()] macro to easily group a set of related routes and minimize repetitive code.

use nestforge::{controller, routes, ApiResult};
use axum::Json;
#[controller("/users")]
pub struct UsersController;
#[routes]
impl UsersController {
#[nestforge::get("/")]
async fn find_all() -> ApiResult<Vec<String>> {
Ok(Json(vec!["Alice".to_string(), "Bob".to_string()]))
}
}

NestForge also supports route versioning through the #[nestforge::version("...")] attribute.

#[controller("/versioning")]
pub struct VersioningController;
#[routes]
impl VersioningController {
#[nestforge::get("/hello")]
#[nestforge::version("1")]
async fn hello_v1() -> String {
"Hello from API v1".to_string()
}
#[nestforge::get("/hello")]
#[nestforge::version("2")]
async fn hello_v2() -> String {
"Hello from API v2".to_string()
}
}

When the application bootstrap uses .with_version("v1"), the version segment becomes part of the mounted route structure.

Handlers often need access to the client request details. NestForge provides a set of extractors that you can use in your handler’s signature.

ExtractorDescription
Param<T>Route parameters (e.g., :id)
Query<T>Query string parameters
Body<T>The request body (JSON)
HeadersRequest headers
CookiesRequest cookies
Inject<T>Resolve a provider from the DI container
ValidatedBody<T>JSON body with automatic validation
#[nestforge::get("/{id}")]
async fn find_one(id: Param<u64>) -> ApiResult<UserDto> {
let user_id = id.value();
// ...
}
#[derive(Deserialize)]
struct PaginationQuery {
page: Option<usize>,
limit: Option<usize>,
}
#[nestforge::get("/")]
async fn find_all(query: Query<PaginationQuery>) -> ApiResult<Vec<UserDto>> {
let page = query.page.unwrap_or(1);
// ...
}

Our previous example of the POST route handler didn’t accept any client params. Let’s fix this by adding the Body extractor here.

But first, we need to determine the DTO (Data Transfer Object) schema. A DTO is an object that defines how the data will be sent over the network.

#[nestforge::dto]
pub struct CreateUserDto {
pub name: String,
pub age: i32,
}
#[nestforge::post("/")]
async fn create(body: Body<CreateUserDto>) -> ApiResult<UserDto> {
let dto = body.value();
// ...
}

NestForge provides a ValidatedBody<T> extractor that automatically validates the incoming request body against your DTO rules.

#[nestforge::dto]
pub struct CreateUserDto {
#[validate(length(min = 3))]
pub name: String,
#[validate(range(min = 18, max = 100))]
pub age: i32,
}
#[nestforge::post("/")]
async fn create(body: ValidatedBody<CreateUserDto>) -> ApiResult<UserDto> {
// If validation fails, NestForge returns a 400 Bad Request automatically.
let dto = body.value();
// ...
}

By default, the response status code is always 200 OK, except for POST requests which are 201 Created. We can easily change this behavior by returning a custom result or using the HttpException type.

#[nestforge::post("/")]
async fn create() -> ApiResult<UserDto> {
if something_failed {
return Err(HttpException::bad_request("Invalid data").into());
}
// ...
}

To maintain a consistent API structure, you might want to wrap all your responses in a standard “envelope”.

#[nestforge::get("/")]
async fn find_all() -> ApiResult<Vec<UserDto>> {
let data = vec![...];
Ok(ResponseEnvelope::paginated(data, total, page, limit).into())
}

This would produce a JSON response like:

{
"data": [...],
"meta": {
"total": 100,
"page": 1,
"limit": 10
}
}