DTOs, Services, and Routes
Understand the normal flow from request DTO to service call to HTTP response in a NestForge feature.
For most HTTP features in NestForge, the request flow is:
- a controller route receives the request
- the route validates and extracts input through DTOs and extractors
- the controller calls an injected service
- the service returns a DTO or domain value
- the controller returns the HTTP response
If a new user understands that sequence, the rest of the framework becomes much easier to navigate.
DTOs define the input and output shapes
Section titled “DTOs define the input and output shapes”The users example has separate DTOs for read, create, and update operations.
Read DTO
Section titled “Read DTO”#[nestforge::dto]pub struct UserDto { pub id: u64, pub name: String, pub email: String,}
nestforge::impl_identifiable!(UserDto, id);This is the shape stored by ResourceService<UserDto> and returned to the client.
Create DTO
Section titled “Create DTO”#[nestforge::dto]pub struct CreateUserDto { #[validate(required)] pub name: String, #[validate(required, email)] pub email: String,}Use create DTOs for required fields and validation rules that must pass before the service is called.
Update DTO
Section titled “Update DTO”#[nestforge::dto]pub struct UpdateUserDto { pub name: Option<String>, #[validate(email)] pub email: Option<String>,}Use update DTOs when partial updates should be allowed.
Services hold the feature behavior
Section titled “Services hold the feature behavior”The users example uses an in-memory resource service:
pub type UsersService = ResourceService<UserDto>;It is registered with seed data:
pub fn users_service_seed() -> UsersService { ResourceService::with_seed(vec![ UserDto { id: 1, name: "John Doe".to_string(), email: "john.doe@example.com".to_string(), }, UserDto { id: 2, name: "Sam".to_string(), email: "sam@example.com".to_string(), }, ])}Then small service helpers wrap the CRUD operations:
pub fn create_user(service: &UsersService, dto: CreateUserDto) -> Result<UserDto, ResourceError> { service.create(dto)}
pub fn update_user( service: &UsersService, id: u64, dto: UpdateUserDto,) -> Result<Option<UserDto>, ResourceError> { service.update(id, dto)}This keeps controllers thin and keeps data behavior in one place.
Controllers expose the routes
Section titled “Controllers expose the routes”The controller is where route metadata, validation, and provider injection meet:
#[controller("/users")]pub struct UsersController;
#[routes]impl UsersController { #[nestforge::post("/")] #[nestforge::version("1")] async fn create( users: Inject<UsersService>, body: ValidatedBody<CreateUserDto>, ) -> ApiResult<UserDto> { let user = create_user(users.as_ref(), body.value()).or_bad_request()?; Ok(Json(user)) }}Important parts of that handler:
#[controller("/users")]sets the base path#[nestforge::post("/")]declares the HTTP method and routeInject<UsersService>resolves the provider from the DI containerValidatedBody<CreateUserDto>validates the request body.or_bad_request()?maps a service error into an HTTP error
How the full route path is produced
Section titled “How the full route path is produced”If the app uses:
.with_global_prefix("api")and the handler uses:
#[nestforge::version("1")]#[controller("/users")]#[nestforge::post("/")]the final route becomes:
/api/v1/usersA practical rule for new users
Section titled “A practical rule for new users”When deciding where code belongs:
- put request shape and validation in DTOs
- put CRUD or business behavior in services
- put HTTP mapping and framework metadata in controllers
- put registration and visibility rules in modules
That separation is the main thing that keeps a NestForge app readable.
What to read next
Section titled “What to read next”If you want the full end-to-end sequence, continue with Build Your First Feature.