Description

This example shows how to make a simple TODO app using Actix Web and a shared Shuttle Postgres DB.

The following routes are provided:

  • GET /todos/<id> - Get a to-do item by ID.
  • POST /todos - Create a to-do item. Takes “note” as a JSON body parameter.

You can clone the example below by running the following (you’ll need cargo-shuttle installed):

cargo shuttle init --from shuttle-hq/shuttle-examples \
 --subfolder actix-web/postgres

Code

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>> {
// query database to get data
// if error, return Bad Request HTTP status code 
    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>> {
// query database to create a new record using the request body
// if error, return Bad Request HTTP status code 
    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> {
// run migrations
    pool.execute(include_str!("../schema.sql"))
        .await
        .map_err(CustomError::new)?;

// set up AppState
    let state = web::Data::new(AppState { pool });

// set up our Actix web service and wrap it with logger and add the AppState as app data
    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,
}

Usage

Once you’ve cloned the example, try launching it locally using cargo shuttle run. Once you’ve verified that it runs successfully, try using cURL in a new terminal to send a POST request:

curl -X POST -d '{"note":"Hello world!"}' -H 'Content-Type: application/json' \
 http://localhost:8000/todo

Assuming the request was successful, you’ll get back a JSON response with the ID and Note of the record you just created. If you try the following cURL command, you should be able to then retrieve the message you stored:

curl http://localhost:8000/todo/<id>

Interested in extending this example? Here’s as couple of ideas:

  • Add update and delete routes
  • Add static files to show your records

If you want to explore other frameworks, we have more examples with popular ones like Tower and Warp. You can find them right here.

Be sure to check out the examples repo for many more examples!