1. Examples
  2. Thruster

Hello world!

Simple ‘Hello world’ app using Thruster.

Create a new directory (mkdir) and move into it (cd) — afterwards, execute the following command to initialize shuttle inside with the Thruster boilerplate.

cargo shuttle init --thruster

Make sure that your cargo.toml file looks as the one below — having the right dependencies is key!

Cargo.toml
[package]
name = "hello-world"
version = "0.1.0"
edition = "2021"

[lib]

[dependencies]
shuttle-service = { version = "0.9.0", features = ["web-thruster"] }
thruster = { version = "1.3.0", features = ["hyper_server"] }

Your lib.rs should look like this:

lib.rs
use thruster::{
    context::basic_hyper_context::{generate_context, BasicHyperContext as Ctx, HyperRequest},
    m, middleware_fn, App, HyperServer, MiddlewareNext, MiddlewareResult, ThrusterServer,
};

#[middleware_fn]
async fn hello(mut context: Ctx, _next: MiddlewareNext<Ctx>) -> MiddlewareResult<Ctx> {
    context.body("Hello, World!");
    Ok(context)
}

#[shuttle_service::main]
async fn thruster() -> shuttle_service::ShuttleThruster<HyperServer<Ctx, ()>> {
    Ok(HyperServer::new(
        App::<HyperRequest, Ctx, ()>::create(generate_context, ()).get("/hello", m![hello]),
    ))
}

Finally, to deploy your app, all you need to do is:

cargo shuttle deploy

And your app is live! 🎉🎉🎉

AWS RDS Postgres

Create a new directory (mkdir) and move into it (cd) — afterwards, execute the following command to initialize shuttle inside with the Tide boilerplate.

cargo shuttle init --thruster

Make sure that your cargo.toml file looks as the one below — having the right dependencies is key!

Cargo.toml
[package]
name = "postgres"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
hyper = "0.14.20"
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0" }
shuttle-aws-rds = { version = "0.9.0", features = ["postgres"] }
shuttle-service = { version = "0.9.0", features = ["web-thruster"] }
sqlx = { version = "0.6", features = ["runtime-tokio-native-tls", "postgres"] }
thruster = { version = "1.3.0", features = ["hyper_server"] }

Your lib.rs should look like this:

lib.rs
use serde::{Deserialize, Serialize};
use shuttle_service::error::CustomError;
use sqlx::{Executor, FromRow, PgPool};
use thruster::{
    context::{hyper_request::HyperRequest, typed_hyper_context::TypedHyperContext},
    errors::{ErrorSet, ThrusterError},
    m, middleware_fn, App, Context, HyperServer, MiddlewareNext, MiddlewareResult, ThrusterServer,
};

type Ctx = TypedHyperContext<RequestConfig>;

#[derive(Deserialize)]
struct TodoNew {
    pub note: String,
}

#[derive(Serialize, FromRow)]
struct Todo {
    pub id: i32,
    pub note: String,
}

struct ServerConfig {
    pool: PgPool,
}

#[derive(Clone)]
struct RequestConfig {
    pool: PgPool,
}

fn generate_context(request: HyperRequest, state: &ServerConfig, _path: &str) -> Ctx {
    Ctx::new(
        request,
        RequestConfig {
            pool: state.pool.clone(),
        },
    )
}

#[middleware_fn]
async fn retrieve(mut context: Ctx, _next: MiddlewareNext<Ctx>) -> MiddlewareResult<Ctx> {
    let id: i32 = context
        .query_params
        .get("id")
        .ok_or_else(|| {
            ThrusterError::parsing_error(
                Ctx::new_without_request(context.extra.clone()),
                "id is required",
            )
        })?
        .parse()
        .map_err(|_e| {
            ThrusterError::parsing_error(
                Ctx::new_without_request(context.extra.clone()),
                "id must be a number",
            )
        })?;

    let todo: Todo = sqlx::query_as("SELECT * FROM todos WHERE id = $1")
        .bind(id)
        .fetch_one(&context.extra.pool)
        .await
        .map_err(|_e| {
            ThrusterError::not_found_error(Ctx::new_without_request(context.extra.clone()))
        })?;

    context.set_body(serde_json::to_vec(&todo).unwrap());

    Ok(context)
}

#[middleware_fn]
async fn add(context: Ctx, _next: MiddlewareNext<Ctx>) -> MiddlewareResult<Ctx> {
    let extra = context.extra.clone();

    let (body, mut context) = context
        .get_body()
        .await
        .map_err(|_e| ThrusterError::generic_error(Ctx::new_without_request(extra)))?;
    let data: TodoNew = serde_json::from_str(&body).map_err(|_e| {
        ThrusterError::generic_error(Ctx::new_without_request(context.extra.clone()))
    })?;

    let todo: Todo = sqlx::query_as("INSERT INTO todos(note) VALUES ($1) RETURNING id, note")
        .bind(&data.note)
        .fetch_one(&context.extra.pool)
        .await
        .map_err(|_e| {
            ThrusterError::generic_error(Ctx::new_without_request(context.extra.clone()))
        })?;

    context.set_body(serde_json::to_vec(&todo).unwrap());

    Ok(context)
}

#[shuttle_service::main]
async fn thruster(
    #[shuttle_aws_rds::Postgres] pool: PgPool,
) -> shuttle_service::ShuttleThruster<HyperServer<Ctx, ServerConfig>> {
    pool.execute(include_str!("../schema.sql"))
        .await
        .map_err(CustomError::new)?;

    Ok(HyperServer::new(
        App::<HyperRequest, Ctx, ServerConfig>::create(generate_context, ServerConfig { pool })
            .post("/todos", m![add])
            .get("/todos/:id", m![retrieve]),
    ))
}

And your schema.sql should look like this:

schema.sql
DROP TABLE IF EXISTS todos;

CREATE TABLE todos (
  id serial PRIMARY KEY,
  note TEXT NOT NULL
);

Finally, to deploy your app, all you need to do is:

cargo shuttle deploy

And your app is live! 🎉🎉🎉