Skip to content

CLI

The nestrs CLI scaffolds projects in two layoutsstandalone (one crate) or workspace (monorepo with crates/features/ + apps/*). nestrs new picks the layout automatically from the directory tree — no mode flags, no config file. Generated code follows the same decorators as the framework repo (users/ for features, api/ for composition apps).

Terminal window
cargo install --locked nest-rs-cli
nestrs version

Requires Rust 1.96+ (edition 2024). That single install is all you need: the first time you call nestrs run, the CLI installs the dev toolchain it drives — just, bacon, cargo-nextest — once. Run nestrs doctor after install to check the Rust toolchain and list optional NESTRS_* environment variables.

nestrs new infers where you are and what to create. There is no nestrs-cli.yaml — the workspace shape is already declared in the root Cargo.toml (members = ["crates/*", "apps/*"]). The CLI walks up from the current directory (or -o path) and reads that manifest.

Where you runCommandResult
Outside a nestrs workspacenestrs new helloMonorepo at ./hello/ + apps/hello/ on port 3000
Inside a nestrs workspacenestrs new blogThin app at apps/blog/next free port in module.rs
Anywhere (explicit)nestrs new hello --standaloneSingle crate at ./hello/

If apps/blog/ already exists, the CLI stops with app blog already exists — it does not overwrite.

Standalone (--standalone)Workspace (default)
LayoutOne crate — logic in src/Monorepo — crates/features/ + apps/*
Depsnest-rs-* from crates.ioSame, via [workspace.dependencies]
GrowCopy patterns by handnestrs g feature / g resource / g <transport>, nestrs new <app>
WhenOne binary, no shared product crateSeveral apps sharing features
Terminal window
nestrs new hello
cd hello
nestrs run dev hello

Open http://localhost:3000/Hello World. Logic in crates/features/src/hello/; apps/hello/ composes modules only.

  • Directoryhello/
    • Cargo.toml
    • Justfile
    • test.just
    • db.just
    • compose.yml
    • .env
    • .env.development
    • .env.example
    • .gitignore
    • rust-toolchain.toml
    • README.md
    • Directorycrates/
      • Directoryfeatures/
        • Cargo.toml
        • Directorysrc/
          • Directoryhello/
            • http/…
            • service.rs
          • lib.rs
      • Directorymigrations/ SeaORM migrations + the migrate binary
      • Directoryseed/ the seed binary
    • Directoryapps/
      • Directoryhello/
        • Directorysrc/
          • lib.rs
          • main.rs
          • module.rs
        • Directorytests/
          • integration/main.rs
          • e2e/main.rs

The e2e suite is scaffolded empty. It has to exist: nestrs run test unit filters on not binary(e2e) and nestrs run test e2e on binary(e2e), and nextest rejects a filterset naming a binary the workspace does not have.

Grow the workspace (from the workspace root or any sub-folder):

Terminal window
nestrs new blog
nestrs g resource posts

Dockerfile and .dockerignore ship in standalone mode only. rust-toolchain.toml pins Rust 1.96 in both modes.

There is nothing to choose. Every path out of nestrs new writes the same hello module — a service with a greeting and one #[public] GET / that returns it:

What you runWhat you getGET /
nestrs new acmemonorepo + hello feature + apps/hello/ on 3000200 Hello World
nestrs new blog (inside one)blog feature + apps/blog/ on the next free port200 Hello World
nestrs new solo --standaloneone crate, service.rs + controller.rs200 Hello World

A freshly created project has to prove it started, and a 404 proves nothing to someone looking at a browser — so there is no routeless variant to pick. Delete the feature once you have a real one; nestrs new blog refuses if a blog feature already exists, rather than overwriting it.

Ports: the CLI scans existing apps/*/src/module.rs and pins the next free one in code (same pattern as auth → 3001, api → 3002, …). Workspace .env files do not set NESTRS_HTTP__PORT — ports live in each app’s module.rs:

HttpModule::for_root(HttpConfig { port: 3002, ..Default::default() })

Optional: --check runs cargo check after generation.

From the workspace root (or apps/, crates/features/, …):

Terminal window
nestrs new blog
nestrs run dev blog

Creates a thin app under apps/blog/ — composition root only, no service.rs / controller.rs in the app crate — plus the blog feature it serves under crates/features/src/blog/. That split is why the greeting is a feature rather than a controller in the app: the layout keeps no business logic in an app crate, so the same rule that governs your code governs the starter.

Every project task runs through nestrs run <recipe> — the single front door. It forwards the recipe and its arguments verbatim to just, which reads the Justfile the scaffolder wrote:

Terminal window
nestrs run dev # watch mode (rebuild + restart on save)
nestrs run test unit # unit + integration
nestrs run db up # apply migrations
nestrs run # list the available recipes

The recipes are yours to edit. Add your own to the Justfile and they run the same way — nestrs run deploy, nestrs run docker-up, whatever you write. The framework owns the recipes it ships; you own the rest.

The first nestrs run installs the toolchain it drives (just, bacon, cargo-nextest) once. On CI, where you provision tools yourself, set NESTRS_NO_BOOTSTRAP=1 or pass --no-bootstrap — a missing tool then fails with the manual install command instead of fetching anything.

Release binaries land in target/release/. nestrs run start compiles and starts the server; use nestrs run build when you only need the artifact (Docker, CI, or copying the binary elsewhere).

Terminal window
nestrs run build # default app (hello)
nestrs run build blog # after nestrs new blog
nestrs run build --all # every app in the workspace
nestrs run start hello # build + run in release

Monorepo only. Generators scaffold under crates/features/src/<name>/, register pub mod … in crates/features/src/lib.rs, and — when you run them from inside an app — wire the edge module into that app’s module.rs. Every command takes --dry-run (print what would change, write nothing) and a -p path override (default: auto-discover from the current directory). Re-running a generator is safe: it refuses to overwrite an existing feature or adapter, and the wiring edits are idempotent.

A port is the transport-agnostic core (service + module). An adapter bolts one transport onto a port. A resource is a DB-backed CRUD port plus its HTTP adapter in one shot.

Terminal window
# Port only (service + module, no transport)
nestrs g feature posts
# DB-backed CRUD: #[expose] entity + CrudService + guarded #[crud] controller
nestrs g resource posts
# The app's authn/authz adapter — Claims, AuthnGuard, AppAbility, AuthzGuard
nestrs g auth
# A SeaORM migration, registered in lib.rs and migrator.rs
nestrs g migration create_posts
# Bolt a transport onto an existing port
nestrs g http posts # controller
nestrs g graphql posts # resolver
nestrs g ws posts # gateway
nestrs g queue posts # processor
nestrs g schedule posts # scheduled tasks
nestrs g mcp posts # MCP tool

Each adapter delegates to the port’s service, so a freshly-generated port plus any adapter compiles immediately — the handler is the seam you fill in. The generator prints the import line and the app-level module each transport needs (HttpModule, GraphqlModule, ScheduleModule, …).

When the cursor isn’t inside an app, wire the edge module by hand:

use features::posts::PostsHttpModule;
#[module(imports = [
// …
PostsHttpModule,
])]
pub struct ApiModule;

g resource emits the #[crud] + #[use_guards(AuthnGuard, AuthzGuard)] form, and scaffolds the g auth adapter when the workspace has none. That is not hardening you can defer: Repo filters every read by the caller’s ambient Ability, which only the ability guard installs, so an unguarded DB-backed controller mounts its routes and then serves nothing.

A generated resource therefore answers 401 without a token and 403 until AppAbility grants a rule for its entity — the two lines the generator prints. A resource meant to be readable without a login is the same slice with one route marked #[public] and a rule in define_visitor; see Public reads. Before inventing a second layout, open crates/features/src/orgs/ side by side with what the generator emitted.

Terminal window
nestrs doctor

Checks:

  • rustc ≥ 1.96 and cargo on PATH
  • whether the current directory sits inside a nestrs workspace
  • optional env vars (NESTRS_DATABASE__URL, NESTRS_QUEUE__URL, …)

Use it after install and before running DB- or Redis-backed apps.

Terminal window
nestrs version
# → NestRS x.y.z
nestrs about
# → version, tagline, docs, repository, license, author
nestrs update
# → checks crates.io; skips when already on the latest version
nestrs update --force
# → cargo install --force nest-rs-cli (reinstall even at the same version)

Contributors inside the monorepo clone:

Terminal window
nestrs update --from-path
# → cargo install --locked --path crates/nest-rs-cli --force
CommandDescription
nestrs new <name>Monorepo at ./<name>/, or app at apps/<name>/ when already inside one
nestrs new <name> --standaloneSingle crate in ./<name>/
nestrs new <name> --checkRun cargo check after scaffolding
nestrs doctorToolchain and env sanity check
nestrs versionPrint the CLI version
nestrs aboutPrint project metadata
nestrs updateInstall latest CLI from crates.io when a newer version exists
nestrs update --forceReinstall from crates.io even when already on the latest version
nestrs update --from-pathReinstall from monorepo crates/nest-rs-cli
nestrs g feature <name>Transport-agnostic port in crates/features/src/
nestrs g resource <name>DB-backed CRUD port + guarded HTTP adapter (deps auto-added)
nestrs g authThe app’s authn/authz adapter — one per workspace
nestrs g migration <name>SeaORM migration, registered in lib.rs and migrator.rs
nestrs g http|graphql|ws|queue|schedule|mcp <name>Add one transport adapter to a port

Every generator accepts --dry-run and -p <path>. Aliases: nestrs g = nestrs generate.

Built by YV17labs