Skip to content

Scaffold the app

You add the blog app to your workspace with the nestrs CLI — the same tool from Getting started. By the end of this page, nestrs run dev blog listens on the port pinned in module.rs and answers Hello World on GET / — the scaffold proves itself before you write a line.

From the workspace root:

  1. Scaffold the app and the feature it serves.

    Terminal window
    nestrs new blog

    Inside an existing workspace this writes apps/blog/ and picks the next free HTTP port in module.rs (see nestrs new blog in the CLI docs). The command refuses to overwrite if the folder already exists.

  2. Start it under bacon (restarts on save).

    Terminal window
    nestrs run dev blog
  3. Hit the root route — use the port from apps/blog/src/module.rs.

    Terminal window
    $ curl -i http://localhost:3005/
    HTTP/1.1 200 OK
    Hello World

Need the full CLI surface — layout detection, --check, generators? See the CLI reference.

nestrs new blog does not ask you to hand-edit the root Cargo.toml members list — workspace apps are discovered from apps/*. It writes a thin app plus the feature behind that greeting: main.rs boots the framework, module.rs composes transports and imports one feature module, and there is no controller.rs or service.rs in the app crate — those live in crates/features/, which is where posts lands from the next page onward.

  • Directoryapps/blog/
    • Cargo.toml
    • Directorysrc/
      • lib.rs
      • main.rs
      • module.rs
    • Directorytests/
      • integration/main.rs
      • e2e/main.rs
  • Directorycrates/features/src/blog/
    • mod.rs
    • module.rs
    • service.rs the greeting
    • Directoryhttp/
      • mod.rs
      • module.rs
      • controller.rs #[public] GET /

The blog feature is a placeholder with one job: proving the wiring works end to end on the first run. Delete it once posts is serving something real.

apps/blog/Cargo.toml
[package]
name = "blog"
version.workspace = true
edition.workspace = true
publish = false
[dependencies]
features.workspace = true
nest-rs-core.workspace = true
nest-rs-config.workspace = true
nest-rs-http.workspace = true
tokio.workspace = true
anyhow.workspace = true
[dev-dependencies]
nest-rs-testing.workspace = true

The root module is where transports come in. HttpModule::for_root pins the listen port in code — not in .env:

apps/blog/src/module.rs
use nest_rs_core::module;
use nest_rs_http::{HttpConfig, HttpModule};
use features::blog::BlogHttpModule;
#[module(
imports = [
HttpModule::for_root(HttpConfig { port: 3005, ..Default::default() }),
BlogHttpModule,
],
)]
pub struct BlogModule;

The reference workspace pins 3005 for blog. When you scaffold with nestrs new blog, the CLI picks the next free port — yours may differ if other apps already exist in the workspace.

BlogModule has no providers = [...] list. A root module composes feature modules and lets each own its providers — the reference root carries only imports. Note the two BlogModules: this one is the app’s composition root, and features::blog::BlogModule is the feature’s own. Two crates, no collision — the app only ever names the …HttpModule edge.

apps/blog/src/main.rs
use anyhow::Result;
use nest_rs_config::Environment;
use nest_rs_core::App;
use blog::BlogModule;
#[tokio::main]
async fn main() -> Result<()> {
let _environment = Environment::init();
App::builder()
.module::<BlogModule>()
.build()
.await?
.run()
.await
}

App::builder() runs the four boot phases — seeds, collect, factories, register — verifies the access graph, then .run() blocks on the transports. The CLI also writes src/lib.rs (re-exporting BlogModule for the tests in Test it end to end), a smoke test that asserts the greeting, and an empty tests/e2e/main.rs. The e2e suite has to exist even while empty: nestrs run test unit filters on not binary(e2e), and nextest rejects a filterset naming a binary the workspace does not have.

  1. From the workspace root:

    Terminal window
    $ nestrs run dev blog
    Compiling blog v0.1.0
    Running `target/debug/blog`
    INFO nest_rs::routes: mounted route controller=BlogController method=GET path=/ handler=hello
    DEBUG nest_rs::http: transport listening addr=0.0.0.0:3005 tls=false

    The route table prints on every boot, and the DEBUG line shows the port from your module.rs.

  2. In another terminal:

    Terminal window
    $ curl -i http://localhost:3005/
    HTTP/1.1 200 OK
    Hello World
  3. Stop the server with Ctrl-C.

That 200 is the checkpoint: the transport is mounted, the DI graph resolved, the access graph passed, and a controller reached your browser. Everything after this page replaces the greeting with real work.

  • A blog app under apps/blog/, scaffolded by nestrs new blog.
  • A BlogModule that activates the HTTP transport and imports one feature.
  • A blog feature answering Hello World on / — proof the framework boots and serves end to end.
  • Declare the entity — the next step: define the Post entity with #[expose].
  • CLI — the reference for nestrs new and the rest of the CLI surface.

Built by YV17labs