- Examples
- Actix
Examples
Actix
Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust.
❗️ Actix currently segfaults on arm64 machines when trying to run locally. In order to “bypass” this, make sure you are installing shuttle via binary.
Hello world!
Simple ‘Hello world’ app using Actix.
Create a new directory (mkdir
) and move into it (cd
) — afterwards, execute the following command to initialize shuttle inside with the Actix boilerplate.
cargo shuttle init --actix
Make sure that your Cargo.toml
file looks like the one below — having the right dependencies is key!
[package]
name = "hello-world"
version = "0.1.0"
edition = "2021"
[dependencies]
actix-web = "4.3.1"
shuttle-actix-web = { version = "0.13.0" }
shuttle-runtime = { version = "0.13.0" }
tokio = { version = "1.26.0" }
Your main.rs
should look like this:
use actix_web::{get, web::ServiceConfig};
use shuttle_actix_web::ShuttleActixWeb;
#[get("/hello")]
async fn hello_world() -> &'static str {
"Hello World!"
}
#[shuttle_runtime::main]
async fn actix_web(
) -> ShuttleActixWeb<impl FnOnce(&mut ServiceConfig) + Send + Clone + 'static> {
let config = move |cfg: &mut ServiceConfig| {
cfg.service(hello_world);
};
Ok(config.into())
}
Finally, to deploy your app, all you need to do is:
cargo shuttle deploy
And your app is live! 🎉🎉🎉
Todo App (Postgres)
Simple todo app using Actix & Postgres.
Create a new directory (mkdir
) and move into it (cd
) — afterwards, execute the following command to initialize shuttle inside with the Actix boilerplate.
cargo shuttle init --actix
Make sure that your Cargo.toml
file looks like the one below — having the right dependencies is key!
[package]
name = "hello-world"
version = "0.1.0"
edition = "2021"
[dependencies]
actix-web = "4.3.1"
shuttle-actix-web = { version = "0.13.0" }
shuttle-runtime = { version = "0.13.0" }
serde = "1.0.148"
shuttle-shared-db = { version = "0.13.0", features = ["postgres"] }
sqlx = { version = "0.6.2", features = ["runtime-tokio-native-tls", "postgres"] }
tokio = { version = "1.26.0" }
Your main.rs
should look like this:
use actix_web::middleware::Logger;
use actix_web::{
error, get, post,
web::{self, Json, ServiceConfig},
Result,
};
use serde::{Deserialize, Serialize};
use shuttle_actix_web::ShuttleActixWeb;
use shuttle_runtime::{CustomError};
use sqlx::{Executor, FromRow, PgPool};
#[get("/{id}")]
async fn retrieve(path: web::Path<i32>, state: web::Data<AppState>) -> Result<Json<Todo>> {
let todo = sqlx::query_as("SELECT * FROM todos WHERE id = $1")
.bind(*path)
.fetch_one(&state.pool)
.await
.map_err(|e| error::ErrorBadRequest(e.to_string()))?;
Ok(Json(todo))
}
#[post("")]
async fn add(todo: web::Json<TodoNew>, state: web::Data<AppState>) -> Result<Json<Todo>> {
let todo = sqlx::query_as("INSERT INTO todos(note) VALUES ($1) RETURNING id, note")
.bind(&todo.note)
.fetch_one(&state.pool)
.await
.map_err(|e| error::ErrorBadRequest(e.to_string()))?;
Ok(Json(todo))
}
#[derive(Clone)]
struct AppState {
pool: PgPool,
}
#[shuttle_runtime::main]
async fn actix_web(
#[shuttle_shared_db::Postgres] pool: PgPool,
) -> ShuttleActixWeb<impl FnOnce(&mut ServiceConfig) + Send + Clone + 'static> {
pool.execute(include_str!("../schema.sql"))
.await
.map_err(CustomError::new)?;
let state = web::Data::new(AppState { pool });
let config = move |cfg: &mut ServiceConfig| {
cfg.service(
web::scope("/todos")
.wrap(Logger::default())
.service(retrieve)
.service(add)
.app_data(state),
);
};
Ok(config.into())
}
#[derive(Deserialize)]
struct TodoNew {
pub note: String,
}
#[derive(Serialize, Deserialize, FromRow)]
struct Todo {
pub id: i32,
pub note: String,
}
And your schema.sql
should look like this:
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! 🎉🎉🎉
Websocket Actorless
Simple ‘Hello world’ app using Actix.
Create a new directory (mkdir
) and move into it (cd
) — afterwards, execute the following command to initialize shuttle inside with the Actix boilerplate.
cargo shuttle init --actix
Make sure that your Cargo.toml
file looks like the one below — having the right dependencies is key!
[package]
name = "hello-world"
version = "0.1.0"
edition = "2021"
[dependencies]
actix-files = "0.6.2"
actix-web = "4.3.1"
actix-ws = "0.2.5"
chrono = { version = "0.4.23", features = ["serde"] }
futures = "0.3"
reqwest = "0.11"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
shuttle-actix-web = { version = "0.13.0" }
shuttle-runtime = { version = "0.13.0" }
shuttle-static-folder = "0.13.0"
tokio = { version = "1", features = ["rt-multi-thread", "sync"] }
tracing = "0.1"
The structure for this example is:
-> src
-> main.rs
-> static
-> index.html
Your main.rs
should look like this:
use actix_files::NamedFile;
use actix_web::{
web::{self, ServiceConfig},
HttpRequest, HttpResponse, Responder,
};
use actix_ws::Message;
use chrono::{DateTime, Utc};
use futures::StreamExt;
use serde::Serialize;
use shuttle_actix_web::ShuttleActixWeb;
use std::{
sync::{atomic::AtomicUsize, Arc},
time::Duration,
};
use tokio::sync::{mpsc, watch};
const PAUSE_SECS: u64 = 15;
const STATUS_URI: &str = "https://api.shuttle.rs";
type AppState = (
mpsc::UnboundedSender<WsState>,
watch::Receiver<ApiStateMessage>,
);
#[derive(Debug, Clone)]
enum WsState {
Connected,
Disconnected,
}
#[derive(Serialize, Default, Clone, Debug)]
struct ApiStateMessage {
client_count: usize,
origin: String,
date_time: DateTime<Utc>,
is_up: bool,
}
async fn echo_handler(
mut session: actix_ws::Session,
mut msg_stream: actix_ws::MessageStream,
tx: mpsc::UnboundedSender<WsState>,
) {
while let Some(Ok(msg)) = msg_stream.next().await {
match msg {
Message::Ping(bytes) => {
if session.pong(&bytes).await.is_err() {
return;
}
}
Message::Text(s) => {
session.text(s.clone()).await.unwrap();
tracing::info!("Got text, {}", s);
}
_ => break,
}
}
if let Err(e) = tx.send(WsState::Disconnected) {
tracing::error!("Failed to send disconnected state: {e:?}");
}
let _ = session.close(None).await;
}
async fn websocket(
req: HttpRequest,
body: web::Payload,
app_state: web::Data<AppState>,
) -> actix_web::Result<HttpResponse> {
let app_state = app_state.into_inner();
let (response, session, msg_stream) = actix_ws::handle(&req, body)?;
let tx_ws_state = app_state.0.clone();
let tx_ws_state2 = tx_ws_state.clone();
// send connected state
if let Err(e) = tx_ws_state.send(WsState::Connected) {
tracing::error!("Failed to send connected state: {e:?}");
}
// listen for api state changes
let mut session_clone = session.clone();
let mut rx_api_state = app_state.1.clone();
actix_web::rt::spawn(async move {
// adding some delay to avoid getting the first message too soon.
tokio::time::sleep(Duration::from_millis(500)).await;
while rx_api_state.changed().await.is_ok() {
let msg = rx_api_state.borrow().clone();
tracing::info!("Handling ApiStateMessage: {msg:?}");
let msg = serde_json::to_string(&msg).unwrap();
session_clone.text(msg).await.unwrap();
}
});
// echo handler
actix_web::rt::spawn(echo_handler(session, msg_stream, tx_ws_state2));
Ok(response)
}
async fn index() -> impl Responder {
NamedFile::open_async("./static/index.html")
.await
.map_err(|e| actix_web::error::ErrorInternalServerError(e))
}
#[shuttle_runtime::main]
async fn actix_web(
) -> ShuttleActixWeb<impl FnOnce(&mut ServiceConfig) + Send + Clone + 'static> {
// We're going to use channels to communicate between threads.
// api state channel
let (tx_api_state, rx_api_state) = watch::channel(ApiStateMessage::default());
// websocket state channel
let (tx_ws_state, mut rx_ws_state) = mpsc::unbounded_channel::<WsState>();
// create a shared state for the client counter
let client_count = Arc::new(AtomicUsize::new(0));
let client_count2 = client_count.clone();
// share tx_api_state
let shared_tx_api_state = Arc::new(tx_api_state);
let shared_tx_api_state2 = shared_tx_api_state.clone();
// share reqwest client
let client = reqwest::Client::default();
let client2 = client.clone();
// Spawn a thread to continually check the status of the api
tokio::spawn(async move {
let duration = Duration::from_secs(PAUSE_SECS);
loop {
tokio::time::sleep(duration).await;
let is_up = get_api_status(&client).await;
let response = ApiStateMessage {
client_count: client_count.load(std::sync::atomic::Ordering::SeqCst),
origin: "api_update loop".to_string(),
date_time: Utc::now(),
is_up,
};
if shared_tx_api_state.send(response).is_err() {
tracing::error!("Failed to send api state from checker thread");
break;
}
}
});
// spawn a thread to continuously check the status of the websocket connections
tokio::spawn(async move {
while let Some(state) = rx_ws_state.recv().await {
match state {
WsState::Connected => {
tracing::info!("Client connected");
client_count2.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
}
WsState::Disconnected => {
tracing::info!("Client disconnected");
client_count2.fetch_sub(1, std::sync::atomic::Ordering::SeqCst);
}
}
let client_count = client_count2.load(std::sync::atomic::Ordering::SeqCst);
tracing::info!("Client count: {client_count}");
let is_up = get_api_status(&client2).await;
if let Err(e) = shared_tx_api_state2.send(ApiStateMessage {
client_count,
origin: format!("ws_update"),
date_time: Utc::now(),
is_up,
}) {
tracing::error!("Failed to send api state: {e:?}");
}
}
});
let app_state = web::Data::new((tx_ws_state, rx_api_state));
let config = move |cfg: &mut ServiceConfig| {
cfg.service(web::resource("/").route(web::get().to(index)))
.service(
web::resource("/ws")
.app_data(app_state)
.route(web::get().to(websocket)),
);
};
Ok(config.into())
}
async fn get_api_status(client: &reqwest::Client) -> bool {
let response = client.get(STATUS_URI).send().await;
response.is_ok()
}
Your index.html
should look like this:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>WS with Actix</title>
<style>
:root {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
font-size: 18px;
}
input[type='text'] {
font-size: inherit;
}
#log {
width: 30em;
height: 20em;
overflow: auto;
margin: 0.5em 0;
border: 1px solid black;
}
#status {
padding: 0 0.2em;
}
#text {
width: 17em;
padding: 0.5em;
}
.msg {
margin: 0;
padding: 0.25em 0.5em;
}
.msg--status {
/* a light yellow */
background-color: #ffffc9;
}
.msg--message {
/* a light blue */
background-color: #d2f4ff;
}
.msg--error {
background-color: pink;
}
</style>
</head>
<body>
<h1>WebSocket example</h1>
<p>
When you connect you will be notified of the shuttle API status and the
amount of connected users every 15 seconds.
</p>
<p>
You can also send a message to the server and you will get back the echo.
</p>
<div>
<button id="connect">Connect</button>
<span>Status:</span>
<span id="status">disconnected</span>
</div>
<div id="log"></div>
<form id="chatForm">
<input type="text" id="text" />
<input type="submit" id="send" value="Echo" />
</form>
<script>
const $status = document.querySelector('#status');
const $connectButton = document.querySelector('#connect');
const $log = document.querySelector('#log');
const $form = document.querySelector('#chatForm');
const $input = document.querySelector('#text');
/** @type {WebSocket | null} */
var socket = null;
function log(msg, type = 'status') {
$log.innerHTML += ``;
$log.scrollTop += 1000;
}
function connect() {
disconnect();
const { location } = window;
const proto = location.protocol.startsWith('https') ? 'wss' : 'ws';
const wsUri = `${proto}://${location.host}/ws`;
log('Connecting...');
socket = new WebSocket(wsUri);
socket.onopen = () => {
log('Connected');
updateConnectionStatus();
};
socket.onmessage = (ev) => {
log('Received: ' + ev.data, 'message');
};
socket.onclose = () => {
log('Disconnected');
socket = null;
updateConnectionStatus();
};
}
function disconnect() {
if (socket) {
log('Disconnecting...');
socket.close();
socket = null;
updateConnectionStatus();
}
}
function updateConnectionStatus() {
if (socket) {
$status.style.backgroundColor = 'transparent';
$status.style.color = 'green';
$status.textContent = `connected`;
$connectButton.innerHTML = 'Disconnect';
$input.focus();
} else {
$status.style.backgroundColor = 'red';
$status.style.color = 'white';
$status.textContent = 'disconnected';
$connectButton.textContent = 'Connect';
}
}
$connectButton.addEventListener('click', () => {
if (socket) {
disconnect();
} else {
connect();
}
updateConnectionStatus();
});
$form.addEventListener('submit', (ev) => {
ev.preventDefault();
const text = $input.value;
log('Sending: ' + text);
socket.send(text);
$input.value = '';
$input.focus();
});
updateConnectionStatus();
</script>
</body>
</html>
Finally, to deploy your app, all you need to do is:
cargo shuttle deploy
And your app is live! 🎉🎉🎉