Manual contentsCLI ReferenceBrowse 103 chapters
Manual 11 min read

Code Generators

The suprnova make:* family scaffolds the conventional file for each piece of a project - a controller, an action, a middleware, a console command, a domain error, a scheduled task, an Inertia page or props struct, a database migration - and wires the new module into its parent mod.rs (and where needed, src/lib.rs and cmd/main.rs). Reach for them when you'd otherwise be retyping the same boilerplate

  • pub mod x; import line, which is most of the time.

make:controller

Scaffold a controller - a file in src/controllers/ with a single #[handler] async fn named invoke.

suprnova make:controller User
suprnova make:controller order_item

The name is normalised to snake_case for the file name and used as-is for the controller: echo in the response. Only ASCII letters, digits, and _ are accepted - paths like api/User are rejected.

Generated file

// src/controllers/user.rs
use suprnova::{handler, json_response, Request, Response};

#[handler]
pub async fn invoke(_req: Request) -> Response {
    json_response!({
        "controller": "User"
    })
}

What it wires

  1. Writes src/controllers/<name>.rs with the #[handler] fn.
  2. Adds pub mod <name>; to src/controllers/mod.rs (creates the file if it didn't exist).
  3. Prints a hint to add a route in src/routes.rs: .get("/<name>", controllers::<name>::invoke).

See Controllers for the handler contract, extractors, and the routes! macro.


make:action

Scaffold a single-responsibility action - a container-resolvable struct with an async execute method that returns a Result<String, FrameworkError> so the skeleton compiles before you fill in the body.

suprnova make:action CreateUser
suprnova make:action SendNotification

The name is PascalCased; an Action suffix is appended if missing, and the file is the snake-cased struct name.

Generated file

// src/actions/create_user_action.rs
use suprnova::{injectable, FrameworkError};

#[injectable]
pub struct CreateUserAction {
    // Add injected dependencies as fields here, e.g.
    // db: suprnova::DbConnection,
}

impl CreateUserAction {
    pub async fn execute(&self) -> Result<String, FrameworkError> {
        Ok("CreateUserAction executed".to_string())
    }
}

What it wires

  1. Writes src/actions/<snake>.rs.
  2. Adds pub mod <snake>; to src/actions/mod.rs.
  3. #[injectable] registers the action with the container at link time, so any controller can resolve it via App::get::<CreateUserAction>() and call action.execute().await?.

See Actions for the resolve-and-invoke pattern and how actions compose with the container.


make:middleware

Scaffold a middleware - a unit struct that implements suprnova::Middleware. The default body times the inner handler and logs the inbound + outbound events with the per-request id, so it runs end-to-end the first time.

suprnova make:middleware Auth
suprnova make:middleware RateLimit

The name is PascalCased; a Middleware suffix is appended if missing. The file uses the snake-cased base name (without the suffix), e.g. Authsrc/middleware/auth.rs, struct AuthMiddleware.

Generated file

// src/middleware/auth.rs
use std::time::Instant;

use suprnova::{async_trait, current_request_id, Middleware, Next, Request, Response};

pub struct AuthMiddleware;

#[async_trait]
impl Middleware for AuthMiddleware {
    async fn handle(&self, request: Request, next: Next) -> Response {
        let method = request.method().to_string();
        let path = request.path().to_string();
        let request_id = current_request_id()
            .map(|id| id.as_str().to_string())
            .unwrap_or_default();
        let started_at = Instant::now();

        println!(
            "[AuthMiddleware] --> {} {} (request_id={})",
            method, path, request_id,
        );

        let response = next(request).await;

        println!(
            "[AuthMiddleware] <-- {} {} ({} ms, request_id={})",
            method, path, started_at.elapsed().as_millis(), request_id,
        );

        response
    }
}

What it wires

  1. Writes src/middleware/<snake>.rs.
  2. Adds mod <snake>; + pub use <snake>::<StructName>; to src/middleware/mod.rs (creates it if needed).
  3. Prints both the per-route shape (.get("/path", handler).middleware(AuthMiddleware)) and the global shape (global_middleware!(middleware::AuthMiddleware) in bootstrap.rs).

See Middleware for the full chain semantics, ordering, and the global vs per-route distinction.


make:command

Scaffold a console command - a #[derive(clap::Parser, Command)] struct that the per-project console binary picks up via inventory at link time. The default body is a println!("…: not yet implemented") so the command runs immediately.

suprnova make:command CleanCache
suprnova make:command mail:send
suprnova make:command clean-cache

Naming follows three rules:

  • Inputs containing : are used verbatim as the registered command name (Laravel namespace style: db:seed, mail:send).
  • Otherwise the snake-cased fn name is kebabbed for the registered name (CleanCache → command clean-cache).
  • The Rust file and struct are always snake-cased / PascalCased forms of the same identifier.

Generated file

// src/commands/clean_cache.rs
use async_trait::async_trait;
use clap::Parser;
use suprnova::{Command, FrameworkError, TypedCommand};

#[derive(Parser, Command, Debug)]
#[console(name = "clean-cache", description = "TODO: describe what clean-cache does")]
pub struct CleanCache {
    // Add clap-derive args here.
}

#[async_trait]
impl TypedCommand for CleanCache {
    async fn run(self) -> Result<(), FrameworkError> {
        println!("clean-cache: not yet implemented");
        Ok(())
    }
}

What it wires

  1. Writes src/commands/<snake>.rs.
  2. Adds pub mod <snake>; to src/commands/mod.rs (creates it if needed).
  3. Warns loudly if src/lib.rs is missing pub mod commands; - the command won't link into the console binary without it.
  4. Prints the run command: cargo run --bin console -- clean-cache.

See Console for the full typed-command surface, the #[command] shorthand for argv-only handlers, and the per-project console binary's role.


make:error

Scaffold a domain error - a unit struct annotated with #[domain_error] so it carries an HTTP status, a Display message, and a From<…> for FrameworkError impl out of the box.

suprnova make:error UserNotFound
suprnova make:error PaymentFailed

The name is PascalCased for the struct and snake-cased for the file. The default status is 500 and the message is the sentence-cased struct name - change both attributes in the generated file to match the situation.

Generated file

// src/errors/user_not_found.rs
use suprnova::domain_error;

#[domain_error(status = 500, message = "User not found")]
pub struct UserNotFound;

Change status = 500 to whatever fits - 404 for not-found, 402 for payment-required, 403 for forbidden - and edit the message string. For richer payloads, add named fields to the struct and reference them in the message via interpolation in a hand-rolled Display impl (drop the #[domain_error] macro at that point).

What it wires

  1. Writes src/errors/<snake>.rs.
  2. Adds pub mod <snake>; to src/errors/mod.rs (creates it if needed).
  3. Warns about declaring mod errors; in src/lib.rs if the errors/ directory was created fresh.

Using it

Inside a handler returning Response, lift the domain type to a FrameworkError so ? short-circuits cleanly:

use crate::errors::user_not_found::UserNotFound;
use suprnova::FrameworkError;

#[handler]
pub async fn show(req: Request) -> Response {
    let id = req.param("id")?;
    let user = find_user(id).await
        .ok_or_else(|| FrameworkError::from(UserNotFound))?;
    json_response!({ "user": user })
}

The Errors chapter covers the full custom-error story, including when to use #[domain_error] vs AppError::bad_request(…) vs a hand-rolled HttpError impl.


make:task

Scaffold a scheduled task - a unit struct that implements suprnova::Task and prints structured start/finish lines so the scaffold logs progress before you fill in the real body.

suprnova make:task CleanupLogs
suprnova make:task SendReminders

The name is PascalCased; a Task suffix is appended if missing. The file is the snake-cased struct name, e.g. CleanupLogssrc/tasks/cleanup_logs_task.rs.

Generated file

// src/tasks/cleanup_logs_task.rs
use std::time::Instant;

use async_trait::async_trait;
use suprnova::{Task, TaskResult};

pub struct CleanupLogsTask;

impl CleanupLogsTask {
    pub fn new() -> Self {
        Self
    }
}

impl Default for CleanupLogsTask {
    fn default() -> Self {
        Self::new()
    }
}

#[async_trait]
impl Task for CleanupLogsTask {
    async fn handle(&self) -> TaskResult {
        let started_at = Instant::now();
        println!("[CleanupLogsTask] task started");

        // Replace this with the real job.

        println!(
            "[CleanupLogsTask] task finished in {} ms",
            started_at.elapsed().as_millis(),
        );
        Ok(())
    }
}

What it wires

The first make:task invocation does heavier wiring than the other generators - it creates the scheduler's surface in the project from scratch:

  1. Creates src/tasks/ and src/tasks/mod.rs if missing.
  2. Creates src/schedule.rs (the register(schedule: &mut Schedule) entrypoint) if missing.
  3. Declares pub mod schedule; and pub mod tasks; in src/lib.rs.
  4. Inserts .schedule(<crate>::schedule::register) into the Application::new() chain in cmd/main.rs or src/main.rs, immediately before .run().
  5. Writes src/tasks/<snake>.rs and adds it to src/tasks/mod.rs.

Subsequent invocations skip the steps that already ran.

Registering the task

Open src/schedule.rs and add a registration call with the fluent schedule API:

use suprnova::Schedule;
use crate::tasks::CleanupLogsTask;

pub fn register(schedule: &mut Schedule) {
    schedule.add(
        schedule.task(CleanupLogsTask::new())
            .daily()
            .at("03:00")
            .name("cleanup:logs")
            .description("Removes old log files daily"),
    );
}

Then run the scheduler:

suprnova schedule:work   # daemon - checks every minute
suprnova schedule:run    # one-shot - typically called by cron
suprnova schedule:list   # show every registered task

See Scheduling for the full task surface (hourly, weekly, cron(...), between, when, without_overlapping, timezone handling) and CLI Scheduling for the run-as-cron vs run-as-daemon trade.


make:inertia

Scaffold either an Inertia page component (default) or a typed Data struct (--data), depending on the flag. The page generator detects the frontend framework (Svelte 5, React 19, Vue 3.5) from .env and emits the matching file extension.

Page mode (default)

suprnova make:inertia About
suprnova make:inertia UserProfile

The name is PascalCased and the suffix Page is appended if missing, so AboutAboutPage. The file lands in frontend/src/pages/ with the per-frontend extension: AboutPage.svelte for Svelte, AboutPage.tsx for React, AboutPage.vue for Vue.

Example (Svelte):

<!-- frontend/src/pages/AboutPage.svelte -->
<div class="font-sans p-8 max-w-xl mx-auto">
  <h1 class="text-3xl font-bold">AboutPage</h1>
  <p class="mt-2">
    Edit <code class="bg-gray-100 px-1 rounded">frontend/src/pages/AboutPage.svelte</code> to get started.
  </p>
</div>

Render it from a controller:

inertia_response!(&req, "AboutPage", props)

See Frontend Pages and Inertia Responses for the bridge between controllers and pages, partial reloads, and shared props.

Data struct mode (--data)

suprnova make:inertia UserProps --data

Emits a #[derive(Data, Validate)] struct in app/src/props/ (not src/props/ - the app/ prefix is hardcoded so the file lands in the workspace's example/host app):

// app/src/props/user_props.rs
use suprnova::Data;
use validator::Validate;

#[derive(Data, Validate)]
pub struct UserProps {
    pub id: i64,
    // Add fields here.
    //
    // Available field attributes:
    //   #[data(input_only)] - accepted on Deserialize, omitted from Serialize
    //   #[data(output_only)] - rejected on Deserialize, included in Serialize
    //   #[data(allow_include)] - registers as ?include=-eligible (default-deny)
    //
    // For PATCH endpoints, use suprnova::data::Field<T> to distinguish
    // absent from null. For lazy outbound fields, use suprnova::inertia::Prop<T>.
}

Use it in a controller to validate request bodies:

let dto: UserProps = req.validate_json().await?;

make:migration

Scaffold a timestamped SeaORM migration file. Covered in detail in CLI Migrations, which also walks the migrate / migrate:rollback / migrate:status / migrate:fresh / db:sync commands. The short form:

suprnova make:migration create_users_table

The migration name is preserved verbatim and prefixed with a YYYYMMDDHHMMSS_ stamp so files sort chronologically. The generated file lands in migrations/.

See Migrations for the schema-builder surface and Database Testing for the TestDatabase::fresh pattern that runs migrations against an isolated database per test.


generate-types

Emit TypeScript interfaces from every Rust struct annotated with #[derive(InertiaProps)]. The dev server runs this automatically; the standalone command is for CI checks and one-shot regenerations.

suprnova generate-types [--output <PATH>] [--watch]
Option Default Description
-o, --output <PATH> frontend/src/types/inertia-props.ts Output file path
-w, --watch off Watch source files and regenerate on change
# One-shot
suprnova generate-types

# Watch mode (useful when you don't want to run the full dev server)
suprnova generate-types --watch

# Custom output path
suprnova generate-types --output frontend/src/types/props.ts

A Rust shape on the left produces a TypeScript interface on the right:

#[derive(InertiaProps)]
pub struct UserPageProps {
    pub user: User,
    pub posts: Vec<Post>,
}
export interface UserPageProps {
    user: User;
    posts: Post[];
}

See Frontend TypeScript Types for the full mapping table (enums, options, dates, nested structs) and the override hooks.


Why Suprnova diverges

Laravel's php artisan make:* drops a file in the right directory and that's it - PSR-4 autoloading picks the new class up the next time the framework boots. Rust has no equivalent. A file at src/foo/bar.rs isn't compiled into the crate until src/foo/mod.rs declares pub mod bar;, and the parent directory has to be wired up the same way in src/lib.rs.

So every suprnova make:* generator does two things instead of one: it writes the new file and edits the closest mod.rs (and, for make:task and make:command, src/lib.rs and cmd/main.rs as well). That's why every generator prints a Created src/.../mod.rs or Updated src/.../mod.rs line - the wiring is part of the work, not a follow-up step you remember on your own.


Summary

Command Creates Wires into
make:controller <name> src/controllers/<snake>.rs controllers/mod.rs
make:action <Name> src/actions/<snake>_action.rs actions/mod.rs
make:middleware <Name> src/middleware/<snake>.rs middleware/mod.rs
make:command <name> src/commands/<snake>.rs commands/mod.rs (+ warns about lib.rs)
make:error <Name> src/errors/<snake>.rs errors/mod.rs
make:task <Name> src/tasks/<snake>_task.rs tasks/mod.rs, schedule.rs, lib.rs, main.rs
make:inertia <Name> frontend/src/pages/<Name>Page.<ext> (no module wiring)
make:inertia <Name> --data app/src/props/<snake>.rs (no module wiring)
make:migration <name> migrations/YYYYMMDDHHMMSS_<name>.rs (no module wiring)
generate-types frontend/src/types/inertia-props.ts n/a

Next

  • CLI Overview - the full subcommand table
  • Console - the per-project console binary that make:command feeds into
  • Controllers - the handler contract make:controller scaffolds
  • Scheduling - the fluent schedule API used to register tasks generated by make:task
  • CLI Migrations - the migrate / db:sync commands that pair with make:migration