ESC
Type to search...

Rapina 0.5.0

Database migrations, CRUD scaffolding, and documentation improvements

Rapina 0.5.0 shipped on February 18, 2026. This release focused on the database layer and developer tooling, adding migration support built on top of SeaORM and a scaffolding command that generates a full CRUD resource from a single command.


Database migrations

Migration support was added via sea-orm-migration. The CLI generated timestamped migration files, and the framework ran them at startup through the builder chain.

Generating a migration

rapina migrate new created a new migration file under src/migrations/:

rapina migrate new create_users

This generated a timestamped file like src/migrations/m20260218_120000_create_users.rs with empty up and down methods to fill in, and updated src/migrations/mod.rs automatically.

Writing a migration

The generated file used sea-orm-migration conventions directly:

use rapina::sea_orm_migration;
use rapina::migration::prelude::*;

#[derive(DeriveMigrationName)]
pub struct Migration;

#[async_trait]
impl MigrationTrait for Migration {
    async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
        manager
            .create_table(
                Table::create()
                    .table(Users::Table)
                    .col(
                        ColumnDef::new(Users::Id)
                            .integer()
                            .not_null()
                            .auto_increment()
                            .primary_key(),
                    )
                    .col(ColumnDef::new(Users::Name).string().not_null())
                    .to_owned(),
            )
            .await
    }

    async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
        manager
            .drop_table(Table::drop().table(Users::Table).to_owned())
            .await
    }
}

#[derive(DeriveIden)]
enum Users {
    Table,
    Id,
    Name,
}

Wiring migrations at startup

The migrations! macro wired migration modules into the migrator, and .run_migrations() applied pending migrations before the server started accepting requests:

mod migrations;

use rapina::prelude::*;
use rapina::database::DatabaseConfig;

#[tokio::main]
async fn main() -> std::io::Result<()> {
    let router = Router::new()
        .get("/users", list_users);

    Rapina::new()
        .with_database(DatabaseConfig::new("sqlite://app.db?mode=rwc"))
        .await?
        .run_migrations::<migrations::Migrator>()
        .await?
        .router(router)
        .listen("127.0.0.1:3000")
        .await
}

src/migrations/mod.rs declared the migration modules and registered them:

mod m20260218_120000_create_users;

rapina::migrations! {
    m20260218_120000_create_users,
}

.run_migrations() required .with_database() to have been called first. If the database was not configured, startup failed with a clear error.


rapina add resource

rapina add resource scaffolded a complete CRUD resource from the command line. It took a resource name and a list of typed fields:

rapina add resource user name:string email:string active:bool

This generated the following files:

src/users/mod.rs           # Module declarations
src/users/handlers.rs      # list, get, create, update, delete handlers
src/users/dto.rs           # CreateUser, UpdateUser request types
src/users/error.rs         # UserError with IntoApiError + DocumentedError
src/entity.rs              # Appends a schema! {} block (or creates the file)
src/migrations/m{TS}_create_users.rs   # Pre-filled migration
src/migrations/mod.rs      # Updated with mod + migrations! macro entry

The generated handlers were ready to compile and followed the same patterns as handwritten Rapina code. After running the command, it printed the exact wiring instructions for main.rs:

  Next steps:

  1. Add the module declaration to src/main.rs:

     mod users;
     mod entity;
     mod migrations;

  2. Register the routes in your Router:

     use users::handlers::{list_users, get_user, create_user, update_user, delete_user};

     let router = Router::new()
         .get("/users", list_users)
         .get("/users/:id", get_user)
         .post("/users", create_user)
         .put("/users/:id", update_user)
         .delete("/users/:id", delete_user);

  3. Pass the router to Rapina:

     Rapina::new()
         .with_database(DatabaseConfig::new("sqlite://app.db?mode=rwc"))
         .await?
         .run_migrations::<migrations::Migrator>()
         .await?
         .router(router)
         .listen("127.0.0.1:3000")
         .await

Supported field types

Fields used a name:type format. Twelve types were supported:

TypeAliasesRust type
stringString
textString
i32integeri32
i64biginti64
f32floatf32
f64doublef64
boolbooleanbool
uuidUuid
datetimeDateTime
dateDate
decimalDecimal
jsonJson

The command failed fast if the resource directory already existed, to avoid overwriting existing code.


Documentation site UX

The docs site received several UX improvements:

  • Search — Ctrl+K opened a search dialog powered by elasticlunr. Previously search was broken.
  • Dark mode — the site followed the system color preference automatically.
  • Mobile nav — the navigation no longer disappeared on small screens.
  • Prev/next links — each page had navigation links at the bottom to move through the docs without scrolling back to the top.
  • Restructured navigation — content was reorganized into Introduction > Getting Started > Core Concepts > CLI, making the flow clearer for newcomers.
  • Landing page — the landing page was updated with a demo GIF showing the CLI in action and an FAQ section.
  • Installation guide — the guide now covered installing Rust before installing Rapina.

Upgrade by bumping the version in your Cargo.toml:

rapina = "0.5.0"