Warp | RestApi | Postgres | SQLx
Let’s build a basic restful api in warp with Postgres and SQLx .
1
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 = [ "postgres", "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 postgres database.
2
Let’s open a terminal to go to postgres database shell:
in terminal :
psqlThen:
CREATE DATABASE todo_list;To create a database named todo_list;
Then
\c todo_listto use the database todo_list;
Then
CREATE TABLE todos (
id SERIAL PRIMARY KEY,
content VARCHAR(255)
);To create a table named todos , which contains two tables.
In main.rs :
3
#[derive(Deserialize)]
struct TodoReq {
content:String
}
#[derive(Serialize, FromRow)]
struct Todo {
id: i32,
content: String
}We define struct TodoReq to get user input data from request body.
We define a struct Todo for manipulating data from a database .
4
async fn db() -> PgPool {
let pool = sqlx::PgPool::connect("postgresql://mike:123456@localhost:5432/todo_list").await.unwrap();
pool
}We define an async function to connect to postgres database , last we return database pool from this function.
5
async fn get_todo_list(pool: PgPool) -> 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 .
Get Mike Code’s stories in your inbox
Join Medium for free to get updates from this writer.
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.
6
async fn add_todo(todo: TodoReq, pool: PgPool) -> 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 .
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 .
7
async fn get_single_todo(id: i32, pool: PgPool) -> 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.
8
async fn delete_todo(id: i32, pool: PgPool) -> 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 todos , we do nothing.
If we find the todo , we use query function to delete that person by id from database.
9
async fn update_todo(id: i32, todo: TodoReq, pool: PgPool) -> 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 a 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.
10
async fn app(pool: PgPool) -> 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 .
11
#[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 in main.rs
use serde::{Deserialize, Serialize};
use sqlx::{ PgPool, 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;
}
//9
async fn app(pool: PgPool) -> 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)
}
//4
async fn get_todo_list(pool: PgPool) -> 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))
}
//5
async fn add_todo(todo: TodoReq, pool: PgPool) -> 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() )
}
//6
async fn get_single_todo(id: i32, pool: PgPool) -> 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))
}
}
//8
async fn update_todo(id: i32, todo: TodoReq, pool: PgPool) -> 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)
}
}
//7
async fn delete_todo(id: i32, pool: PgPool) -> 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)
}
}
//3
async fn db() -> PgPool {
let pool = sqlx::PgPool::connect("postgresql://mike:123456@localhost:5432/todo_list").await.unwrap();
pool
}
//2
#[derive(Serialize, FromRow)]
struct Todo {
id: i32,
content: String
}
//1
#[derive(Deserialize)]
struct TodoReq {
content:String
}




