Warp | Restful Api | Sqlite | SQLx
Let’s build a basic restful api in warp
in cargo.toml:
[dependencies]
tokio = { version = "1", features = ["full"] }
warp = { version = "0.4", features=["server"]}
serde = {version = "1.0.219", features = ["derive"]}
serde_json = "1.0.140"
sqlx = { version="0.8.6", features = [ "sqlite", "runtime-tokio", "tls-native-tls" ] }We use tokio for asynchronous runtime, serde and serde_json for parse json , warp for backend server , sqlx for connecting sqlite database.
In main.rs:
1
#[derive(Serialize, FromRow)]
struct Todo {
id: i32,
content: String
}
#[derive(Deserialize)]
struct TodoReq {
content:String
}We define a struct Todo for manipulating data from a database .
We define struct TodoReq to get user input data from request body.
2
async fn db() -> SqlitePool {
let pool = sqlx::sqlite::SqlitePool::connect("sqlite://db.sqlite").await.unwrap();
sqlx::query("
CREATE TABLE IF NOT EXISTS todos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
content TEXT
)
").execute(&pool).await.unwrap();
pool
}We define an async function to connect to sqlite database , and also create a table named todos in the database , last we return database pool from this function.
Make sure we need to create a database file named db.sqlite in our project directory .
3
async fn get_todo_list(pool: SqlitePool) -> Result<warp::reply::Json , warp::Rejection> {
let rows : Vec<Todo> = sqlx::query_as("SELECT * FROM todos").fetch_all(&pool).await.unwrap();
let todos_json = serde_json::to_string_pretty(&rows).unwrap();
Ok(warp::reply::json(&todos_json))
}We create get_todo_list handler , we add pool to get the shared state database pool .
We call sqlx::query_as to query data from database, and fetch_all method with argument &pool to get all data from todos table. We add type annotation for variable rows , which is Vec(Todo) .
Then we call serde_json::to_string_pretty and pass the reference to rows as argument to parse rows as json .
Last we return this todos_json as response.
4
async fn add_todo(todo: TodoReq, pool: SqlitePool) -> Result<String , warp::Rejection> {
sqlx::query("INSERT INTO todos (content) VALUES (?1)").bind(&todo.content).execute(&pool).await.unwrap();
Ok("Add new Todo Successful.".to_string() )
}We create add_todo handler, we add parameter pool to get database pool, todo to get todo content from request .
Get Mike Code’s stories in your inbox
Join Medium for free to get updates from this writer.
We call sqlx::query to insert data into todos table , we call bind method to add parameters value which is todo.content , last we call execute method with argument &pool .
5
async fn get_single_todo(id: i32, pool: SqlitePool) -> Result<warp::reply::Json , warp::Rejection> {
let todos: Vec<Todo> = sqlx::query_as("SELECT * FROM todos WHERE id = ?1").bind(&id).fetch_all(&pool).await.unwrap();
if todos.len() == 0 {
let msg = format!("No todo id : {} Found", id);
Ok(warp::reply::json(&msg))
} else {
let todo_json = serde_json::to_string_pretty(&todos[0]).unwrap();
Ok(warp::reply::json(&todo_json))
}
}We create handler get_single_todo , parameter pool to get pool of database , id to get dynamic parameter id in the route path.
We call query_as to select data by adding a where condition , we call bind method to pass the id as parameter’s value , then call execute method with pool to execute this query.
Then we create if else , if length of rows is 0 , it means there is no todo we want to find by id. Else we get the todo by index 0 , and parse it to json , and send it as a response.
6
async fn delete_todo(id: i32, pool: SqlitePool) -> Result<String , warp::Rejection> {
let todos: Vec<Todo> = sqlx::query_as("SELECT * FROM todos WHERE id = ?1").bind(&id).fetch_all(&pool).await.unwrap();
if todos.len() == 0 {
let msg = format!("No todo id : {} Found", id);
Ok(msg)
} else {
sqlx::query("DELETE FROM todos WHERE id = ?1").bind(&id).execute(&pool).await.unwrap();
let msg = format!("Remove Todo id : {} Successful", id);
Ok(msg)
}
}We create a handler delete_todo .
In this handler we first try to get the todo we want to delete by id , then check if there is no todo , we do nothing.
If we find the todo , we use query function to delete that person by id from database.
7
async fn update_todo(id: i32, todo: TodoReq, pool: SqlitePool) -> Result<String , warp::Rejection> {
let todos: Vec<Todo> = sqlx::query_as("SELECT * FROM todos WHERE id = ?1").bind(&id).fetch_all(&pool).await.unwrap();
if todos.len() == 0 {
let msg = format!("No todo id : {} Found", id);
Ok(msg)
} else {
sqlx::query("UPDATE todos SET content = ?1 WHERE id = ?2").bind(&todo.content).bind(&id).execute(&pool).await.unwrap();
let msg = format!("Update Todo id : {} Successful", id);
Ok(msg)
}
}We create handler update_todo ,
We first try to find the person by id .
IF not found the todo , we do nothing to database , just send a response.
If find the todo , we update the todo by id , the change the content field’s value to new value from todo.
8
async fn app(pool: SqlitePool) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
let root = warp::path!().map(|| "Home");
let state_filter = warp::any().map(move || pool.clone());
let todo_list_route = warp::path!("todo_list").and(warp::get()).and(state_filter.clone()).and_then(get_todo_list);
let add_todo_route = warp::path!("add_todo").and(warp::post()).and(warp::body::json()).and(state_filter.clone()).and_then(add_todo);
let single_todo_route = warp::path!("todo" / i32).and(warp::get()).and(state_filter.clone()).and_then(get_single_todo);
let delete_todo_route = warp::path!("delete_todo" / i32 ).and(warp::delete()).and(state_filter.clone()).and_then(delete_todo);
let update_todo_route = warp::path!("update_todo" / i32).and(warp::put()).and(warp::body::json()).and(state_filter.clone()).and_then(update_todo);
root
.or(todo_list_route)
.or(add_todo_route)
.or(single_todo_route)
.or(delete_todo_route)
.or(update_todo_route)
}We defined app function to create a warp router , we defined 5 routes with handlers .
9
#[tokio::main]
async fn main() {
let pool = db().await;
let app = app(pool).await;
warp::serve(app)
.run(([0,0,0,0], 3000))
.await;
}Finally, we just serve warp server in main function.
Full code:
use serde::{Deserialize, Serialize};
use sqlx::{Executor, SqlitePool, prelude::FromRow};
use warp::Filter;
#[tokio::main]
async fn main() {
let pool = db().await;
let app = app(pool).await;
warp::serve(app)
.run(([0,0,0,0], 3000))
.await;
}
//
async fn app(pool: SqlitePool) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
let root = warp::path!().map(|| "Home");
let state_filter = warp::any().map(move || pool.clone());
let todo_list_route = warp::path!("todo_list").and(warp::get()).and(state_filter.clone()).and_then(get_todo_list);
let add_todo_route = warp::path!("add_todo").and(warp::post()).and(warp::body::json()).and(state_filter.clone()).and_then(add_todo);
let single_todo_route = warp::path!("todo" / i32).and(warp::get()).and(state_filter.clone()).and_then(get_single_todo);
let delete_todo_route = warp::path!("delete_todo" / i32 ).and(warp::delete()).and(state_filter.clone()).and_then(delete_todo);
let update_todo_route = warp::path!("update_todo" / i32).and(warp::put()).and(warp::body::json()).and(state_filter.clone()).and_then(update_todo);
root
.or(todo_list_route)
.or(add_todo_route)
.or(single_todo_route)
.or(delete_todo_route)
.or(update_todo_route)
}
//
async fn get_todo_list(pool: SqlitePool) -> Result<warp::reply::Json , warp::Rejection> {
let rows : Vec<Todo> = sqlx::query_as("SELECT * FROM todos").fetch_all(&pool).await.unwrap();
let todos_json = serde_json::to_string_pretty(&rows).unwrap();
Ok(warp::reply::json(&todos_json))
}
//
async fn add_todo(todo: TodoReq, pool: SqlitePool) -> Result<String , warp::Rejection> {
sqlx::query("INSERT INTO todos (content) VALUES (?1)").bind(&todo.content).execute(&pool).await.unwrap();
Ok("Add new Todo Successful.".to_string() )
}
//
async fn get_single_todo(id: i32, pool: SqlitePool) -> Result<warp::reply::Json , warp::Rejection> {
let todos: Vec<Todo> = sqlx::query_as("SELECT * FROM todos WHERE id = ?1").bind(&id).fetch_all(&pool).await.unwrap();
if todos.len() == 0 {
let msg = format!("No todo id : {} Found", id);
Ok(warp::reply::json(&msg))
} else {
let todo_json = serde_json::to_string_pretty(&todos[0]).unwrap();
Ok(warp::reply::json(&todo_json))
}
}
//
async fn update_todo(id: i32, todo: TodoReq, pool: SqlitePool) -> Result<String , warp::Rejection> {
let todos: Vec<Todo> = sqlx::query_as("SELECT * FROM todos WHERE id = ?1").bind(&id).fetch_all(&pool).await.unwrap();
if todos.len() == 0 {
let msg = format!("No todo id : {} Found", id);
Ok(msg)
} else {
sqlx::query("UPDATE todos SET content = ?1 WHERE id = ?2").bind(&todo.content).bind(&id).execute(&pool).await.unwrap();
let msg = format!("Update Todo id : {} Successful", id);
Ok(msg)
}
}
//
async fn delete_todo(id: i32, pool: SqlitePool) -> Result<String , warp::Rejection> {
let todos: Vec<Todo> = sqlx::query_as("SELECT * FROM todos WHERE id = ?1").bind(&id).fetch_all(&pool).await.unwrap();
if todos.len() == 0 {
let msg = format!("No todo id : {} Found", id);
Ok(msg)
} else {
sqlx::query("DELETE FROM todos WHERE id = ?1").bind(&id).execute(&pool).await.unwrap();
let msg = format!("Remove Todo id : {} Successful", id);
Ok(msg)
}
}
//
async fn db() -> SqlitePool {
let pool = sqlx::sqlite::SqlitePool::connect("sqlite://db.sqlite").await.unwrap();
pool.execute("
CREATE TABLE IF NOT EXISTS todos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
content TEXT
)
").await.unwrap();
pool
}
//
#[derive(Serialize, FromRow)]
struct Todo {
id: i32,
content: String
}
//
#[derive(Deserialize)]
struct TodoReq {
content:String
}




