Rust Restaurant Management Project: Building, Testing with Warp & SQLite
Welcome to PlayBook Library’s comprehensive pedagogical study notes for building a production-grade Restaurant Management API Server in Rust. This project guides you through designing an asynchronous web service from scratch using the Warp framework for routing, an embedded SQLite database via Rusqlite for state persistence, Serde for serialization/deserialization, and a custom Rust Client to concurrently test all CRUD and business logic APIs without manual Postman clicks or curl commands.
1. Why Do We Need This? (Problem Statement & Status Quo)
- The Testing Fatigue Problem: Building backend APIs usually requires setting up cumbersome Postman collections, managing environment variables, or writing error-prone
curlterminal commands. Developers often lose 20-30 minutes just validating endpoints. - The Lightweight POC Challenge: Heavy web frameworks (like Actix-web with advanced features) introduce steep learning curves and boilerplate configurations. For Minimum Viable Products (MVPs) and Proofs of Concept (POCs), we need an ultra-lightweight, expressive framework that boots up in a handful of lines.
- Why Rust?: We need thread-safe concurrency, memory safety without a garbage collector, and deterministic resource management, making Rust ideal for high-throughput restaurant order routing.
2. Core Mental Model & Architecture
This project adopts a clean, layered backend architecture: Routes -> Handlers -> Models (Structs) -> Database Layer (Rusqlite).
Architectural Workflow
Client (Rust Concurrency Runner)
|
| HTTP Request (GET / POST / DELETE)
v
Warp Routing Layer (routes.rs)
|
| Extracts Path, Query Params, & JSON Body
v
Handler Layer (handlers.rs)
|
| Executes Business Logic & DB Transactions
v
Model Structs & Rusqlite Layer (models.rs & db.rs)
|
| Executes SQL on restaurant.db
v
SQLite Database Storage
Real-Life Analogy: The Restaurant Floor & Kitchen
Real-Life Analogy: Imagine a restaurant.
- The Tables: Physical booths where customers sit (represented by our
tablesdatabase table).- The Menu: The catalog of items (pizzas, burgers, pasta) offered to diners (
menustable).- The Orders: Tickets pinned to the kitchen counter mapping a table ID to ordered menu items (
ordersandorder_itemstables).- The Waitstaff (Handlers): Take requests from customers, verify if items exist, update quantities, calculate cooking times, and relay instructions back and forth.
3. Project Directory Structure
We structure our workspace into a modular workspace containing an application server (app_server) and a testing client (client_server).
4. Setting Up the Application Entry Point (main.rs)
The main.rs file acts as the binding force of our server. It boots up Tokio as our asynchronous runtime, initializes the SQLite database schema, binds our warp filter routes, and starts the server on 127.0.0.1:3030.
// app_server/src/main.rs
mod db;
mod handlers;
mod models;
mod routes;
use warp::Filter;
#[tokio::main]
async fn main() {
// 1. Initialize SQLite Database & Tables
db::initialize_db();
// 2. Combine Restaurant Routes
let routes = routes::restaurant_routes();
println!("Starting server at http://127.0.0.1:3030");
// 3. Serve HTTP requests with Warp Trace logging
warp::serve(routes)
.with(warp::log("restaurant"))
.run(([127, 0, 0, 1], 3030))
.await;
}
5. Database Initialization & Schema (db.rs)
We use Rusqlite to manage our SQLite database (restaurant.db). We enforce foreign key constraints (pragma foreign_keys = on) to maintain relational integrity between tables, orders, and order items.
// app_server/src/db.rs
use rusqlite::{Connection, Result};
pub fn get_db_connection() -> Connection {
Connection::open("restaurant.db").expect("Failed to open SQLite connection")
}
pub fn initialize_db() {
println!("Initializing database...");
let con = get_db_connection();
// Enable foreign key support in SQLite
con.execute("PRAGMA foreign_keys = ON;", [])
.expect("Failed to enable foreign key support");
println!("Creating tables if not exists...");
// Create Tables
con.execute(
"CREATE TABLE IF NOT EXISTS tables (
id INTEGER PRIMARY KEY AUTOINCREMENT,
code TEXT NOT NULL UNIQUE
);",
[],
).expect("Failed to create tables table");
con.execute(
"CREATE TABLE IF NOT EXISTS menus (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL
);",
[],
).expect("Failed to create menus table");
con.execute(
"CREATE TABLE IF NOT EXISTS orders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
table_id INTEGER NOT NULL,
FOREIGN KEY (table_id) REFERENCES tables(id),
UNIQUE(table_id)
);",
[],
).expect("Failed to create orders table");
con.execute(
"CREATE TABLE IF NOT EXISTS order_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
order_id INTEGER NOT NULL,
menu_id INTEGER NOT NULL,
cooking_time INTEGER NOT NULL,
quantity INTEGER NOT NULL DEFAULT 1,
FOREIGN KEY (order_id) REFERENCES orders(id),
FOREIGN KEY (menu_id) REFERENCES menus(id)
);",
[],
).expect("Failed to create order_items table");
}
6. Data Structures & Domain Models (models.rs)
Models define the shape of our data. We derive Serialize and Deserialize using Serde so Rust structs can be seamlessly converted into JSON payloads over HTTP.
// app_server/src/models.rs
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Table {
#[serde(skip_deserializing)]
pub id: i64,
pub code: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct TableResponse {
pub id: i64,
pub code: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Menu {
#[serde(skip_deserializing)]
pub id: i64,
pub name: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct MenuResponse {
pub id: i64,
pub name: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct OrderRequestBody {
pub table_id: i64,
pub menu_ids: Vec<i64>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct OrderResponse {
pub id: i64,
pub table_id: i64,
pub table_name: String,
pub total_cooking_time: i32,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct OrderItem {
#[serde(skip_deserializing)]
pub id: i64,
pub order_id: i64,
pub menu_id: i64,
pub cooking_time: i32,
pub quantity: i32,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct OrderItemResponse {
pub id: i64,
pub order_id: i64,
pub menu_id: i64,
pub menu_name: String,
pub cooking_time: i32,
pub quantity: i32,
}
7. Warp HTTP Routing Layer (routes.rs)
Warp uses composable filters to declare API paths (/tables, /menus, /orders) and map HTTP verbs (GET, POST, DELETE) directly to our handler functions.
// app_server/src/routes.rs
use warp::{Filter, Rejection, Reply};
use crate::handlers::*;
use crate::db::get_db_connection;
pub fn restaurant_routes() -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
let db_filter = warp::any().map(get_db_connection);
// 1. Tables Routes
let list_tables = warp::path("tables")
.and(warp::get())
.and(db_filter.clone())
.and_then(list_table_handler);
let create_table = warp::path("tables")
.and(warp::path("create"))
.and(warp::post())
.and(db_filter.clone())
.and(warp::body::json())
.and_then(create_table_handler);
// 2. Menu Routes
let list_menus = warp::path("menus")
.and(warp::get())
.and(db_filter.clone())
.and_then(list_menu_handler);
let create_menu = warp::path("menus")
.and(warp::path("create"))
.and(warp::post())
.and(db_filter.clone())
.and(warp::body::json())
.and_then(create_menu_handler);
// 3. Order Routes
let list_orders = warp::path("orders")
.and(warp::get())
.and(db_filter.clone())
.and_then(list_order_handler);
let create_order = warp::path("orders")
.and(warp::path("create"))
.and(warp::post())
.and(db_filter.clone())
.and(warp::body::json())
.and_then(create_order_handler);
// 4. Order Item Specific Routes
let list_order_items = warp::path!("tables" / i64 / "items")
.and(warp::get())
.and(db_filter.clone())
.and_then(list_order_items_for_table_handler);
let delete_order_item = warp::path!("tables" / i64 / "items" / i64)
.and(warp::delete())
.and(db_filter.clone())
.and_then(delete_order_item_handler);
list_tables
.or(create_table)
.or(list_menus)
.or(create_menu)
.or(list_orders)
.or(create_order)
.or(list_order_items)
.or(delete_order_item)
.recover(handle_rejection)
}
async fn handle_rejection(err: Rejection) -> Result<impl Reply, std::convert::Infallible> {
let code;
let message;
if err.is_not_found() {
code = warp::http::StatusCode::NOT_FOUND;
message = "Route not found";
} else if err.find::<warp::body::BodyDeserializeError>().is_some() {
code = warp::http::StatusCode::BAD_REQUEST;
message = "Failed to deserialize request body";
} else {
code = warp::http::StatusCode::INTERNAL_SERVER_ERROR;
message = "Internal Server Error";
}
Ok(warp::reply::with_status(warp::reply::json(&message), code))
}
8. Request Handlers & Business Logic (handlers.rs)
Handlers process incoming route requests, interact with database connection instances, handle conditional upserts (such as incrementing item quantities if an order already exists for a table), and format responses into JSON.
// app_server/src/handlers.rs
use rusqlite::Connection;
use warp::{Rejection, Reply, http::StatusCode};
use crate::models::{Table, Menu, OrderRequestBody};
pub async fn list_table_handler(mut con: Connection) -> Result<impl Reply, Rejection> {
let mut stmt = con.prepare("SELECT id, code FROM tables").unwrap();
let table_iter = stmt.query_map([], |row| {
Ok(Table {
id: row.get(0)?,
code: row.get(1)?,
})
}).unwrap();
let tables: Vec<Table> = table_iter.filter_map(Result::ok).collect();
Ok(warp::reply::with_status(warp::reply::json(&tables), StatusCode::OK))
}
pub async fn create_table_handler(mut con: Connection, body: Table) -> Result<impl Reply, Rejection> {
let res = con.execute("INSERT INTO tables (code) VALUES (?1)", &[&body.code]);
match res {
Ok(_) => Ok(warp::reply::with_status(warp::reply::json(&"Table created successfully"), StatusCode::CREATED)),
Err(_) => Ok(warp::reply::with_status(warp::reply::json(&"Failed to create table"), StatusCode::INTERNAL_SERVER_ERROR)),
}
}
pub async fn list_menu_handler(mut con: Connection) -> Result<impl Reply, Rejection> {
let mut stmt = con.prepare("SELECT id, name FROM menus").unwrap();
let menu_iter = stmt.query_map([], |row| {
Ok(Menu {
id: row.get(0)?,
name: row.get(1)?,
})
}).unwrap();
let menus: Vec<Menu> = menu_iter.filter_map(Result::ok).collect();
Ok(warp::reply::with_status(warp::reply::json(&menus), StatusCode::OK))
}
pub async fn create_menu_handler(mut con: Connection, body: Menu) -> Result<impl Reply, Rejection> {
let res = con.execute("INSERT INTO menus (name) VALUES (?1)", &[&body.name]);
match res {
Ok(_) => Ok(warp::reply::with_status(warp::reply::json(&"Menu created successfully"), StatusCode::CREATED)),
Err(_) => Ok(warp::reply::with_status(warp::reply::json(&"Failed to create menu"), StatusCode::INTERNAL_SERVER_ERROR)),
}
}
pub async fn create_order_handler(mut con: Connection, body: OrderRequestBody) -> Result<impl Reply, Rejection> {
if body.menu_ids.is_empty() {
return Ok(warp::reply::with_status(warp::reply::json(&"Please add items to order"), StatusCode::BAD_REQUEST));
}
// Check if an order already exists for this table
let existing_order: Result<i64, _> = con.query_row(
"SELECT id FROM orders WHERE table_id = ?1",
&[body.table_id],
|row| row.get(0),
);
let order_id = match existing_order {
Ok(id) => id,
Err(_) => {
con.execute("INSERT INTO orders (table_id) VALUES (?1)", &[&body.table_id]).unwrap();
con.last_insert_rowid()
}
};
// Add items to order
for menu_id in body.menu_ids {
let cooking_time = 15; // Simulated cooking time in minutes
let existing_item: Result<i64, _> = con.query_row(
"SELECT id FROM order_items WHERE order_id = ?1 AND menu_id = ?2",
&[order_id, menu_id],
|row| row.get(0),
);
match existing_item {
Ok(item_id) => {
con.execute(
"UPDATE order_items SET quantity = quantity + 1 WHERE id = ?1",
&[item_id],
).unwrap();
}
Err(_) => {
con.execute(
"INSERT INTO order_items (order_id, menu_id, cooking_time, quantity) VALUES (?1, ?2, ?3, 1)",
&[order_id, menu_id, cooking_time],
).unwrap();
}
}
}
Ok(warp::reply::with_status(warp::reply::json(&"Order created/updated successfully"), StatusCode::CREATED))
}
pub async fn list_order_handler(mut con: Connection) -> Result<impl Reply, Rejection> {
let mut stmt = con.prepare("SELECT o.id, t.id, t.code FROM orders o JOIN tables t ON o.table_id = t.id").unwrap();
let order_iter = stmt.query_map([], |row| {
Ok(crate::models::OrderResponse {
id: row.get(0)?,
table_id: row.get(1)?,
table_name: row.get(2)?,
total_cooking_time: 30, // Aggregate calculation placeholder
})
}).unwrap();
let orders: Vec<_> = order_iter.filter_map(Result::ok).collect();
Ok(warp::reply::with_status(warp::reply::json(&orders), StatusCode::OK))
}
pub async fn list_order_items_for_table_handler(table_id: i64, mut con: Connection) -> Result<impl Reply, Rejection> {
let mut stmt = con.prepare(
"SELECT oi.id, oi.order_id, oi.menu_id, m.name, oi.cooking_time, oi.quantity
FROM order_items oi
JOIN orders o ON oi.order_id = o.id
JOIN menus m ON oi.menu_id = m.id
WHERE o.table_id = ?1"
).unwrap();
let items_iter = stmt.query_map(&[table_id], |row| {
Ok(crate::models::OrderItemResponse {
id: row.get(0)?,
order_id: row.get(1)?,
menu_id: row.get(2)?,
menu_name: row.get(3)?,
cooking_time: row.get(4)?,
quantity: row.get(5)?,
})
}).unwrap();
let items: Vec<_> = items_iter.filter_map(Result::ok).collect();
Ok(warp::reply::with_status(warp::reply::json(&items), StatusCode::OK))
}
pub async fn delete_order_item_handler(table_id: i64, menu_id: i64, mut con: Connection) -> Result<impl Reply, Rejection> {
let res = con.execute(
"DELETE FROM order_items WHERE menu_id = ?1 AND order_id IN (SELECT id FROM orders WHERE table_id = ?2)",
&[&menu_id, &table_id],
);
match res {
Ok(_) => Ok(warp::reply::with_status(warp::reply::json(&"Order item deleted successfully"), StatusCode::OK)),
Err(_) => Ok(warp::reply::with_status(warp::reply::json(&"Failed to delete order item"), StatusCode::INTERNAL_SERVER_ERROR)),
}
}
9. Automated Rust Client for Concurrency Testing (client_server/src/main.rs)
Instead of manual Postman testing, we build an automated concurrent Rust client using the reqwest crate. This client boots up, seeds tables and menu items, simulates concurrent restaurant orders, queries tables, and removes items.
// client_server/src/main.rs
use reqwest::Client;
use serde_json::json;
use tokio::time::{sleep, Duration};
#[tokio::main]
async fn main() {
println!("Starting Rust Test Client...");
let client = Client::new();
let base_url = "http://127.0.0.1:3030";
// 1. Create Tables Concurrently
let table_codes = vec!["T1", "T2", "T3", "T4", "T5"];
for code in &table_codes {
let res = client.post(format!("{}/tables/create", base_url))
.json(&json!({ "code": code }))
.send()
.await;
match res {
Ok(_) => println!("Created table: {}", code),
Err(e) => eprintln!("Error creating table {}: {}", code, e),
}
}
// 2. Create Menu Items
let menu_names = vec!["Pizza Margherita", "Garlic Bread", "Pasta Alfredo", "Coke Zero", "Tiramisu"];
for name in &menu_names {
let res = client.post(format!("{}/menus/create", base_url))
.json(&json!({ "name": name }))
.send()
.await;
match res {
Ok(_) => println!("Created menu item: {}", name),
Err(e) => eprintln!("Error creating menu {}: {}", name, e),
}
}
// 3. Simulate Concurrent Orders
sleep(Duration::from_secs(1)).await;
println!("\n--- Simulating Orders Concurrently ---");
let order_res = client.post(format!("{}/orders/create", base_url))
.json(&json!({ "table_id": 1, "menu_ids": [1, 2, 3] }))
.send()
.await;
if let Ok(resp) = order_res {
println!("Order creation response status: {}", resp.status());
}
// 4. Fetch Order Items for Table 1
sleep(Duration::from_secs(1)).await;
let items_res = client.get(format!("{}/tables/1/items", base_url))
.send()
.await;
if let Ok(resp) = items_res {
let items: serde_json::Value = resp.json().await.unwrap_or_default();
println!("Items for Table 1: {:#?}", items);
}
// 5. Delete an Item from Table 1's Order
sleep(Duration::from_secs(1)).await;
let delete_res = client.delete(format!("{}/tables/1/items/2", base_url))
.send()
.await;
if let Ok(resp) = delete_res {
println!("Delete item response status: {}", resp.status());
}
println!("Client simulation completed successfully!");
}
10. Dependency Management (Cargo.toml)
Ensure your Cargo.toml files include the necessary asynchronous runtime (tokio), web routing (warp), serialization (serde, serde_json), and HTTP client (reqwest) libraries.
App Server Cargo.toml
[package]
name = "restaurant_warp_app"
version = "0.1.0"
edition = "2021"
[dependencies]
tokio = { version = "1.0", features = ["full"] }
warp = "0.3"
rusqlite = { version = "0.29", features = ["bundled"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
Client Server Cargo.toml
[package]
name = "restaurant_warp_client"
version = "0.1.0"
edition = "2021"
[dependencies]
tokio = { version = "1.0", features = ["full"] }
reqwest = { version = "0.11", features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
11. Protocol & Framework Comparison Table
12. Summary and Key Takeaways
- Warp Composability: Warp uses a filter-based approach (
warp::path,warp::post,warp::body::json) that reads almost like natural language, making routing clean and modular. - Embedded SQLite Persistence: Using
rusqlitewith foreign key pragma enabled (PRAGMA foreign_keys = ON) ensures relational integrity across tables, menus, orders, and order items without heavyweight database servers. - Serde Serialization: Deriving
SerializeandDeserializebridges the gap between Rust strongly-typed structs and incoming/outgoing JSON payloads. - Automated Rust Testing Clients: Writing a companion client script using
reqwestandtokioallows developers to test concurrent API workflows instantly without relying on fragile Postman collections. - Modular Separation of Concerns: Decoupling the system into
main.rs,db.rs,models.rs,routes.rs, andhandlers.rskeeps the codebase maintainable and extensible.