Sitemap

Warp | RestApi | Postgres | SQLx

5 min readOct 5, 2026

--

Let’s build a basic restful api in warp with Postgres and SQLx .

(Youtube Video)

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 :

psql

Then:

CREATE DATABASE todo_list;

To create a database named todo_list;

Then

\c todo_list

to 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
}

--

--