Deploying an Axum URL Shortener on Shuttle |Rust.

I found Shuttle weeks ago while I was scrolling through Reddit, I found it really cool, and I want to try it immediately, and I thought it would be a great opportunity to use Axum, that which is a web framework I started to use recently. I don't have much experience with Rust, and it is my first time using Shuttle. Be patient with me, please.

If you don't know what Shuttle is, this is what its Github page says:

Shuttle is a serverless platform for Rust which makes it really easy to deploy your web-apps.

Shuttle is built for productivity, reliability, and performance:

  • Zero-Configuration support for Rust using annotations
  • Automatic resource provisioning (databases, caches, subdomains, etc.) via Infrastructure-From-Code
  • First-class support for popular Rust frameworks (Rocket, Axum, Tide, and Tower)
  • Scalable hosting (with optional self-hosting)

First, we need to install the cargo-shuttle subcommand:

cargo install cargo-shuttle

After the subcommand is installed, we are ready to create our project. We will build a URL Shortener, like the example on Shuttle's web page, but their example is built with Rocket, you can see them here. Our URL Shortener will be built with Axum.

cargo shuttle init url-shrtnr

This is our project's directory structure:

url-shortener/
    --src/  
        -- lib.rs
    -- Cargo.toml
    -- .gitignore

Let's add the dependencies to our project:

Cargo.toml

[package]
name = "url-shrtnr"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
shuttle-service = { version = "0.4.2", features = ["web-axum", "sqlx-postgres"] }
axum = "0.5.15"
serde = "1.0.137"
sqlx = { version = "0.5.13", features = ["runtime-tokio-native-tls", "postgres"] }
sync_wrapper = "0.1.1"

Now, let's write a Hello World example.

Hello World example

lib.rs

use axum::{
    routing::{get},
    Router,
};
use shuttle_service::ShuttleAxum;
use sync_wrapper:: SyncWrapper;


#[shuttle_service::main]
async fn main() -> ShuttleAxum {

    let router = Router::new()
        .route("/", get(root));

    let sync_wrapper = SyncWrapper::new(router);

    Ok(sync_wrapper)

}

async fn root() -> &'static str {
    "Hello, World!"
}

Now we run this command in our terminal:

cargo shuttle run

We should see this message in our terminal:

Starting url-shrtnr on http://127.0.0.1:8000

If we use our browser or curl and paste the URL, we will see the message "Hello, World!".

Now, to deploy the app we need to log in first, let's go to this page and get our API key.

Then in our terminal, we write this command:

cargo shuttle login --api-key <your api-key>
cargo shuttle deploy

If everything is ok, we will see this message:

 Project:            url-shrtnr
        Deployment Id:      9ebd7ed5-0fa2-4d43-a1ef-3168c1ad22e6
        Deployment Status:  DEPLOYED
        Host:               https://url-shrtnr.shuttleapp.rs
        Created At:         2022-06-29 15:01:37.557012238 UTC

We will see the message "Hello, World!" in our browser if we go to the URL "https://url-shrtnr.shuttleapp.rs".

If we have an error message saying that there's another app with the same name, we have to create Shuttle.toml in the same location Cargo.toml is. Inside Shuttle.toml we write the name we want for our app.

Shuttle.toml

name = "<your app's name>"

According to the documentation:

If the name key is not specified, the service’s name will be the same as the crate’s name.

Alternatively, you can override the project name on the command-line, by passing the –name argument:

cargo shuttle deploy --name=$PROJECT_NAME

Now, let's write our URL shortener. We will use Sqlx for our database and sqlx-cli to generate our migrations folder. If you haven't installed sqlx-cli, you can install it with the next command:

cargo install sqlx-cli
sqlx database create --database-url postgres://<your user>:<your password>@localhost:<port>/<your database name>
sqlx migrate add <database>

We should see this message in our terminal:

Creating migrations\20220629154910_url.sql

Congratulations on creating your first migration!

Did you know you can embed your migrations in your application binary?
On startup, after creating your database connection or pool, add:

sqlx::migrate!().run(<&your_pool OR &mut your_connection>).await?;

Note that the compiler won't pick up new migrations if no Rust source files have changed.
You can create a Cargo build script to work around this with `sqlx migrate build-script`.

See: https://docs.rs/sqlx/0.5/sqlx/macro.migrate.html

After that we go to migrations/<timestap>-url.sql and there we add our table.

CREATE TABLE url (
  id VARCHAR(6) PRIMARY KEY,
  url VARCHAR NOT NULL
);

Then we need to run migrations.

sqlx migrate run --database-url postgres://<your user>:<your password>@localhost:<port>/<database>

Let's add other dependencies to our Cargo.toml:

Cargo.toml

[dependencies]
...

nanoid = "0.4"
url ="2.2"

lib.rs

use axum::{
    routing::{get, post},
    Router,
    response::Redirect,
    http::StatusCode,
    extract::Extension,
};
use shuttle_service::{error::CustomError, ShuttleAxum};
use sync_wrapper:: SyncWrapper;
use serde::Serialize;
use sqlx::migrate::Migrator;
use sqlx::{FromRow, PgPool};
use url::Url;



#[derive(Serialize, FromRow)]
struct StoredURL {
    pub id: String,
    pub url: String,
}
async fn shorten(url:String, Extension(pool): Extension<PgPool>) -> Result<String, StatusCode> {
    let id = &nanoid::nanoid!(6);

    let parserd_url = Url::parse(&url).map_err(|_err| {
        StatusCode::UNPROCESSABLE_ENTITY
    })?;

    sqlx::query("INSERT INTO url(id, url) VALUES ($1, $2)")
        .bind(id)
        .bind(parserd_url.as_str())
        .execute(&pool)
        .await
        .map_err(|_| {
            StatusCode::INTERNAL_SERVER_ERROR
        })?;

        Ok(format!("https://url-shrtnr.shuttleapp.rs/{id}"))
}

We pass a URL to the shorten function, then, we define a parsed_url to check if the string passed is a URL and return an error message if is not.

Then, we pass the SQL query to the query function and bind the values id generated by nanoid and the url. If an error is not raised, it will return and url as a string and the id as the shortened url.

async fn redirect(id: String, Extension(pool): Extension<PgPool>) -> Result<Redirect, StatusCode> {
    let stored_url: StoredURL = sqlx::query_as("SELECT * FROM url WHERE id = $1")
        .bind(id)
        .fetch_one(&pool)
        .await
        .map_err(|err| match err {
            sqlx::Error::RowNotFound => StatusCode::NOT_FOUND,
            _=> StatusCode::INTERNAL_SERVER_ERROR

        })?;

        Ok(Redirect::to(&stored_url.url))
}

The redirect function retrieves a URL from the database when its id is passed. If the id doesn't exist in the database, it will return a Not Found error.

static MIGRATOR: Migrator = sqlx::migrate!();

#[shuttle_service::main]
async fn axum(pgpool: PgPool) -> ShuttleAxum {

    MIGRATOR.run(&pgpool).await.map_err(CustomError::new)?;

    let router = Router::new()
        .route("/:id", get(redirect))
        .route("/:url", post(shorten))
        .layer(Extension(pgpool));

    let sync_wrapper = SyncWrapper::new(router);

    Ok(sync_wrapper)

}

If everything is ok, our URL-Shortener will be deployed and we will see the next message:

Compiling url-shrtnr v0.1.0 (/opt/shuttle/crates/url-shrtnr)
    Finished dev [unoptimized + debuginfo] target(s) in 49.18s

        Project:            url-shrtnr
        Deployment Id:      771d387b-fb1c-48ce-97a5-ff3452036265
        Deployment Status:  DEPLOYED
        Host:               https://url-shrtnr.shuttleapp.rs
        Created At:         2022-06-29 16:37:02.890391021 UTC
        Database URI:       postgres://***:***@pg.shuttle.rs:5432/db-url-shrtnr

Conclusion

It was really fun for me to build and deploy this app, it is the first time I deployed anything, and it was easy with Shuttle, they wrote very good documentation with examples for Rocket, Axum, Tide, and Postgres, and the examples were very helpful.

If there is anything I didn't explain or I should improve or know about Shuttle, Axum, Sqlx, or Rust, please leave a comment.

Here is the complete source code.

Thank you for taking the time to read this article.

If you have any recommendations about other packages, architectures, how to improve my code, my English, or anything; please leave a comment or contact me through Twitter, LinkedIn.

References:

3
Subscribe to my newsletter

Read articles from Carlos Armando Marcano Vargas directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Carlos Armando Marcano Vargas
Carlos Armando Marcano Vargas

I am a backend developer from Venezuela. I enjoy writing tutorials for open source projects I using and find interesting. Mostly I write tutorials about Python, Go, and Rust.