Tutorial: Building a TODO List API
This tutorial demonstrates how to build a complete RESTful API for managing a list of TODOs. It brings together many of warp
's features, including routing, JSON body handling, shared state, and simple authentication.
The full source code for this example can be found in examples/todos.rs
.
Our API will have the following endpoints:
GET /todos
: List all TODOs.POST /todos
: Create a new TODO.PUT /todos/:id
: Update a TODO.DELETE /todos/:id
: Delete a TODO.
1. Data Model and State
First, let's define our data structures. For simplicity, we'll use an in-memory database—a Vec
of Todo
items protected by a Mutex
for safe concurrent access.
// From `examples/todos.rs` (models module)
use serde_derive::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::Mutex;
// The in-memory database type.
pub type Db = Arc<Mutex<Vec<Todo>>>;
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Todo {
pub id: u64,
pub text: String,
pub completed: bool,
}
// Query parameters for listing todos, e.g., /todos?offset=1&limit=10
#[derive(Debug, Deserialize)]
pub struct ListOptions {
pub offset: Option<usize>,
pub limit: Option<usize>,
}
2. Filters
We'll structure our application by creating a Filter
for each endpoint. These filters will be combined to form the complete API.
Helper Filters
To avoid repetition, we can create smaller helper filters.
// A filter to share the Db instance with handlers.
fn with_db(db: Db) -> impl Filter<Extract = (Db,), Error = std::convert::Infallible> + Clone {
warp::any().map(move || db.clone())
}
// A filter to deserialize a JSON body of type Todo.
fn json_body() -> impl Filter<Extract = (Todo,), Error = warp::Rejection> + Clone {
warp::body::content_length_limit(1024 * 16).and(warp::body::json())
}
Endpoint Filters
Now we can define a filter for each CRUD operation.
// GET /todos?offset=3&limit=5
pub fn todos_list(db: Db) -> impl Filter<Extract = (impl warp::Reply,), Error = warp::Rejection> + Clone {
warp::path!("todos")
.and(warp::get())
.and(warp::query::<ListOptions>())
.and(with_db(db))
.and_then(handlers::list_todos)
}
// POST /todos with JSON body
pub fn todos_create(db: Db) -> impl Filter<Extract = (impl warp::Reply,), Error = warp::Rejection> + Clone {
warp::path!("todos")
.and(warp::post())
.and(json_body())
.and(with_db(db))
.and_then(handlers::create_todo)
}
// PUT /todos/:id with JSON body
pub fn todos_update(db: Db) -> impl Filter<Extract = (impl warp::Reply,), Error = warp::Rejection> + Clone {
warp::path!("todos" / u64)
.and(warp::put())
.and(json_body())
.and(with_db(db))
.and_then(handlers::update_todo)
}
// DELETE /todos/:id
pub fn todos_delete(db: Db) -> impl Filter<Extract = (impl warp::Reply,), Error = warp::Rejection> + Clone {
// A simple auth check: require 'authorization: Bearer admin'
let admin_only = warp::header::exact("authorization", "Bearer admin");
warp::path!("todos" / u64)
.and(admin_only)
.and(warp::delete())
.and(with_db(db))
.and_then(handlers::delete_todo)
}
3. Handlers
The and_then
combinator passes the extracted values to our handler functions. These functions perform the actual logic and return a Result<impl Reply, Infallible>
.
// From `examples/todos.rs` (handlers module)
// Handler for GET /todos
pub async fn list_todos(opts: ListOptions, db: Db) -> Result<impl warp::Reply, Infallible> {
let todos = db.lock().await;
// Apply query options for pagination
let todos: Vec<Todo> = todos.clone().into_iter()
.skip(opts.offset.unwrap_or(0))
.take(opts.limit.unwrap_or(std::usize::MAX))
.collect();
Ok(warp::reply::json(&todos))
}
// Handler for POST /todos
pub async fn create_todo(create: Todo, db: Db) -> Result<impl warp::Reply, Infallible> {
let mut vec = db.lock().await;
// Check for duplicate ID
for todo in vec.iter() {
if todo.id == create.id {
return Ok(StatusCode::BAD_REQUEST);
}
}
vec.push(create);
Ok(StatusCode::CREATED)
}
// ... other handlers for update and delete ...
4. Putting It All Together
Finally, in our main
function, we initialize the database, combine all the endpoint filters with .or()
, add a logger, and start the server.
// From `examples/todos.rs`
#[tokio::main]
async fn main() {
let db = models::blank_db();
let api = filters::todos_list(db.clone())
.or(filters::todos_create(db.clone()))
.or(filters::todos_update(db.clone()))
.or(filters::todos_delete(db));
let routes = api.with(warp::log("todos"));
warp::serve(routes).run(([127, 0, 0, 1], 3030)).await;
}
This example showcases how warp
's filter system allows you to build a structured, maintainable, and type-safe web API by composing small, reusable pieces of logic.