Skip to content

First Steps

Your first hands-on encounter with NestForge. Let's build a simple Users feature.

In this guide, you’ll learn the fundamental building blocks of NestForge by building a simple “Users” feature. We’ll cover modules, services, DTOs, and controllers.

  1. Generate the Feature

    NestForge provides a powerful CLI to speed up development. Instead of creating files manually, let’s use the generator.

    Terminal window
    # Generate a module
    nestforge g module users
    # Generate a service and controller within that module
    nestforge g resource users --module users

    This creates a structured folder at src/users/ with everything we need.

  2. Define a Data Transfer Object (DTO)

    Before we write business logic, we need to define the “shape” of the data entering and leaving our API. We’ll create a UserDto for output and a CreateUserDto for input.

    src/users/dto/user_dto.rs
    #[nestforge::dto]
    pub struct UserDto {
    pub id: u64,
    pub name: String,
    pub email: String,
    }
    // src/users/dto/create_user_dto.rs
    #[nestforge::dto]
    pub struct CreateUserDto {
    #[validate(length(min = 3))]
    pub name: String,
    #[validate(email)]
    pub email: String,
    }
  3. Build the Service

    Applications need a place for “business logic”. In NestForge, this happens in Services. For this example, we’ll use a built-in ResourceService which provides an in-memory storage for quick prototyping.

    src/users/services/users_service.rs
    use nestforge::ResourceService;
    use crate::users::dto::{UserDto, CreateUserDto};
    pub type UsersService = ResourceService<UserDto>;
    // A simple factory to seed our service with initial data
    pub fn users_service_factory() -> UsersService {
    ResourceService::with_seed(vec![
    UserDto { id: 1, name: "Alice".into(), email: "alice@example.com".into() }
    ])
    }
  4. Expose the API via a Controller

    Now we need to make our service reachable over HTTP. We’ll define a controller and inject our UsersService.

    src/users/controllers/users_controller.rs
    use nestforge::{controller, routes, Inject, ApiResult, ValidatedBody};
    use axum::Json;
    use crate::users::dto::{UserDto, CreateUserDto};
    use crate::users::services::UsersService;
    #[controller("/users")]
    pub struct UsersController;
    #[routes]
    impl UsersController {
    #[nestforge::get("/")]
    async fn list(users: Inject<UsersService>) -> ApiResult<Vec<UserDto>> {
    // as_ref() gives us the service instance from the wrapper
    let all = users.all().await;
    Ok(Json(all))
    }
    #[nestforge::post("/")]
    async fn create(
    users: Inject<UsersService>,
    body: ValidatedBody<CreateUserDto>
    ) -> ApiResult<UserDto> {
    let newUser = users.create(body.value()).await?;
    Ok(Json(newUser))
    }
    }
  5. Wire it up in the Module

    We need to tell NestForge about our new controller and service. We do this in the UsersModule.

    src/users/mod.rs
    use nestforge::module;
    use self::controllers::UsersController;
    use self::services::users_service_factory;
    #[module(
    controllers = [UsersController],
    providers = [users_service_factory()],
    exports = []
    )]
    pub struct UsersModule;
  6. Import to the Root Module

    Finally, import the UsersModule into your AppModule so the framework knows it exists.

    src/app_module.rs
    #[module(
    imports = [UsersModule],
    controllers = [AppController],
    providers = [],
    )]
    pub struct AppModule;
  7. Test the Results

    Start your application:

    Terminal window
    cargo run

    Try hitting your new endpoint:

    Terminal window
    curl http://localhost:3000/users
  • DTOs define the data contract.
  • Services contain the logic.
  • Controllers handle the HTTP transport.
  • Modules act as the glue, wiring everything into the application’s dependency injection system.

Notice the ValidatedBody<CreateUserDto> in our controller? Because we added #[validate] attributes to our DTO, NestForge automatically validates the incoming JSON. If a user sends an invalid email, they’ll get a 400 Bad Request with a helpful error message—without you writing a single if statement!