Migrating to Shuttle

Cover image

Hey! Today we're going to have a quick look at migrating a project that uses the Tokio runtime with Axum, to Shuttle.

Setup

Pre-requisites

Before we start, make sure you have the latest version of cargo-shuttle installed.

For this example, we'll assume you are migrating an Axum project that has a database.

Migrating your project

Add your dependencies

The first step is to add shuttle-runtime and shuttle-axum to your dependencies to be able to use Shuttle's runtime with the Axum framework.

cargo add shuttle-runtime shuttle-axum

Any secrets you need to use will be kept in a Secrets.toml file (dev secrets in Secrets.dev.toml) which will be placed at the Cargo.toml level.

You can also easily get a provisioned database like so (this example will be for a provisioned PostgreSQL instance specifically):

cargo add shuttle-shared-db --features postgres

If you have any database records you'd like to keep, it would be a good idea to export them so that they can be migrated to the new database. You will not need a secrets file if you only need a provisioned Postgres database - this will be automatically be provisioned and given to you in the form of a connection string or an sqlx pool.

Migrating your code

To be able to run your project on Shuttle, you need to make a few changes to your code. Instead of the tokio::main macro, you will use the shuttle_runtime::main macro and swap out dotenvy for Shuttle's Postgres annotation:

This is what your main.rs file might look like before:

#[tokio::main]
async fn main() {
    dotenvy::dotenv().ok();

    let url = dotenvy::var("DATABASE_URL").expect("No database URL was set!");

    let pool = sqlx::Pool::connect(&url).await.unwrap();

    sqlx::migrate!()
        .run(&pool)
        .await
        .expect("Migrations failed :(");

    let router = create_api_router(pool);
    let addr = SocketAddr::from(([0, 0, 0, 0], 8000));

    Server::bind(&addr)
        .serve(router.into_make_service())
        .await
        .unwrap()
}

And this is what it looks like after:

#[shuttle_runtime::main]
pub async fn axum (
    #[shuttle_shared_db::Postgres] pool: PgPool,
    #[shuttle_runtime::Secrets] secrets: shuttle_runtime::SecretStore,
) -> shuttle_axum::ShuttleAxum {
    sqlx::migrate!()
        .run(&pool)
        .await
        .expect("Migrations failed :(");

    // Use secrets for anything that needs them

    let router = create_api_router(pool);

    Ok(router.into())
}

If you need more than a simple router, you'll want to create a custom struct that holds all of your required app state information inside and then create an impl for the struct - you can find more about that here. Anything outside of your entry point function (the function that uses the shuttle_runtime::main macro) doesn't need to be changed. If you are using secrets as well as a database connection, you may wish to create a struct that holds both of these values and then pass it into the function that generates the router. Interested? We also have some additional docs on how to do this

Deploying

To ensure that you get a unique project name, create a Shuttle.toml file at the Cargo.toml level to name your project to whatever you like.

name = "my-unique-app-name-here"

Now all you need to do is to run the following commands:

shuttle project start
shuttle project deploy

Your project should now be deployed!

Finishing up

Thanks for reading! Hopefully this article has helped you learn what it takes to migrate to Shuttle.

Further reading:

This blog post is powered by shuttle - The Rust-native, open source, cloud development platform. If you have any questions, or want to provide feedback, join our Discord server!
Share article
rocket

Build the Future of Backend Development with us

Join the movement and help revolutionize the world of backend development. Together, we can create the future!