Skip to content

Migrations

Schema changes live in the migrations crate — a product crate under crates/, scaffolded by nestrs new alongside seed. Migrations are SeaORM’s, so a migration is a struct implementing MigrationTrait with an up and a down. Neither the API nor the worker run migrations on startup; you run them explicitly with nestrs run db.

Terminal window
nestrs g migration create_org

One file per migration, named m<utc-date>_<seq>_<what>.rs. DeriveMigrationName takes the version from the file name; a DeriveIden enum names the table and its columns so there are no stringly-typed identifiers. Fill in the columns the generator stubbed:

crates/migrations/src/m20260526_000000_create_org.rs
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Org::Table)
.if_not_exists()
.col(ColumnDef::new(Org::Id).uuid().not_null().primary_key())
.col(ColumnDef::new(Org::Name).string().not_null().unique_key())
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Org::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum Org {
Table,
Id,
Name,
}

down is the inverse of up — it’s what nestrs run db down and nestrs run db fresh replay to roll back cleanly.

A later migration references an earlier table by adding a foreign key — the user table hangs off org:

crates/migrations/src/m20260526_000001_create_user.rs
.col(ColumnDef::new(User::OrgId).uuid().not_null())
.foreign_key(
ForeignKey::create()
.name("fk_user_org_id")
.from(User::Table, User::OrgId)
.to(Org::Table, Org::Id)
.on_delete(ForeignKeyAction::Restrict)
.on_update(ForeignKeyAction::Cascade),
)

Two registrations, both written by g migration: the mod line in lib.rs, and the ordered vec in migrator.rs. Migrations run top to bottom, so a table must appear after anything it references.

crates/migrations/src/lib.rs
mod m20260526_000000_create_org;
mod m20260526_000001_create_user;
mod migrator;
pub use migrator::{Migrator, migrate};

migrator.rs is regenerated from that mod list on every g migration, so the vec can never fall behind the files:

crates/migrations/src/migrator.rs
use sea_orm_migration::prelude::*;
use super::{m20260526_000000_create_org, m20260526_000001_create_user};
pub struct Migrator;
#[async_trait::async_trait]
impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![
Box::new(m20260526_000000_create_org::Migration),
Box::new(m20260526_000001_create_user::Migration),
]
}
}

Writing a migration by hand means writing both — the one you forget is the one that silently never runs.

Terminal window
nestrs run db up # apply every pending migration
nestrs run db down # roll back the last applied migration
nestrs run db status # show applied vs. pending
nestrs run db fresh # drop every table, then re-apply from scratch

Each verb shells out to the crate’s migrate binary at crates/migrations/src/bin/migrate.rs. It connects through nest_rs_seaorm::connect_from_env() — the single connector for tools outside the DI container, which resolves NESTRS_DATABASE__* through the same .env cascade the apps use. Reaching for std::env::var("NESTRS_DATABASE__URL") in a tool of your own works only when the variable is really in the process environment; connect_from_env works either way.

  • Seeding — load demo data once the schema is in place.
  • Database — the data layer the schema backs.

Built by YV17labs