Notas en construcción
Bookstore API. Axum, SQLx y PostgreSQL
- Ejemplo de aplicación utilizando Axum, SQLx y PostgreSQL
- Para separar el código se crean diferentes archivos y módulos o secciones, para que Rust los reconozca se deben agregar al
main.rso al/modulo/mod.rslocal
Dependencias
Cargo.toml
[package]
name = "app_name"
version = "0.1.0"
edition = "2021"
[dependencies]
# Servidor
tokio = { version = "1.40.0", features = ["full"] }
tower = "0.5.0"
axum = "0.7.5"
# Base de datos (PostgreSQL)
sqlx = { version = "0.8.1", features = [
"runtime-tokio",
"tls-rustls-ring",
"postgres",
"uuid",
"time",
"rust_decimal",
] }
# Para algunos tipos de la base de datos
uuid = { version = "1.10.0", features = ["v4", "serde"] }
time = { version = "=0.3.36" }
rust_decimal = "1.36"
# Serializar a JSON y deserializar de JSON
serde = { version = "1.0.209", features = ["derive"] }
serde_json = "1.0.127"
# Leer archivo .env
dotenvy = "0.15.7"
- Deserialización, convertir un objeto JSON a un Struct, Enum u otro tipo de dato de Rust
- Serialización, convertir de Struc, Enum o algún tipo de dato de Rust a JSON
.env
- Crear archivo con las variables de entorno
- Crear
.env.template - Agregar
.enval.gitignorey si existe,.dockerignore - Agregar los pasos de construcción al
README.md
.env
SERVER_ADDR=0.0.0.0:8080
# Debe llamarse así si se trabaja con las macros! de SQLx
DATABASE_URL="postgres://postgres:@localhost:5433/bookstore-api"
Server
- Convertir función principal a
async - Agregar
#[tokio::main]a la función principal - Importar (con
mod) todos los archivos o módulos del proyecto - Si no se importan, el compilador no reconoce o escanea los archivos
src/main.rs
use std::env;
use tokio::net::TcpListener;
use dotenvy::dotenv;
// Importar modulos o archivos del proyecto
mod books;
mod config;
mod router;
use crate::router::router;
// Crear runtime asíncrono
#[tokio::main]
async fn main() {
// Inicializar dotenvy para poder leer las variables del archivo .env
dotenv().ok().expect("Failed to load .env file");
let server_addr = env::var("SERVER_ADDR").expect("SERVER_ADDR undefined");
// Obtener configuración con todas las rutas
//let routes = Router::new().route("/", get(|| async { "Hello world" }));
let router = router().await;
// Crear TCP Listener
let listener = TcpListener::bind(server_addr).await.unwrap();
println!("Listening on: {}", listener.local_addr().unwrap());
// Iniciar servidor
axum::serve(listener, router)
.await
.expect("Server can't initialize");
}
DB Pool
- La variable debe llamarse DATABASE_URL si se trabaja con macros! de SQLx
- Se comparte todo el pool de conexiones entre las rutas al pasarlo como
State()en la sección de rutas (routes.rs)
src\config.rs
use std::{env, time::Duration};
use sqlx::{postgres::PgPoolOptions, Pool, Postgres};
pub async fn get_db_pool() -> Pool<Postgres> {
// Obtener cadena de conexión
// La variable debe llamarse DATABASE_URL si se trabaja con macros! de SQLx
let db_url = env::var("DATABASE_URL").expect("DATABASE_URL undefined");
// Crear y exportar pool de conexiones
let pool = PgPoolOptions::new()
.max_connections(5)
.acquire_timeout(Duration::from_secs(5))
.connect(&db_url)
.await
.expect("Failed to connect with te Database");
pool
}
Rutas
router()devuelve todas las rutas de los diferentes módulos, se utiliza enmain.rsal inicializar el servidornestse utiliza para agrupar rutas en un mismo prefijo,/users, /books, /products- Se dividen para agruparlas fácilmente por rutas o prefijos
- Para aplicar middlewares específicos a cada sección
- Se obtiene o crea un pool de conexiones a la base de datos de forma asíncrona
- Todas las rutas comparten el pool de conexiones con
with_state(db_pool)
use axum::{routing::get, Router};
use crate::{books::books_routes, config::get_db_pool};
pub async fn router() -> Router {
// Obtener o crear el pool de la base de datos
let db_pool = get_db_pool().await;
// Dividir las rutas por módulos o secciones
// - Para agruparlas facilmente por rutas o prefijos
// - Para aplicar middlewares especificos a cada sección
let authenticated_routes = Router::new().nest("/books", books_routes());
// Unir y exportar todas las rutas
let routes = Router::new()
.route("/", get(hello()))
.nest("/api", authenticated_routes)
.with_state(db_pool);
routes
}
fn hello() -> &'static str {
"Axum!"
}
/modulo
Rutas mod.rs
- Similar a un controller, define las rutas o métodos disponibles y su respectivo handler o servicio
- Se exportan todas las rutas para poder utilizarse en el
src/router.rsglobal - Debe llamarse
/<nombre>/mod.rspara ser reconocido - Si hay más archivos en el módulo o carpeta, se deben agregar con
mod archivo1;mod archivo1;
use axum::{
routing::{delete, get, patch, post},
Router,
};
use handler::{create, delete_book, get_all, get_by_uuid, test_sqlx_query, update};
use sqlx::PgPool;
mod handler;
mod types;
pub fn books_routes() -> Router<PgPool> {
let routes = Router::new()
.route("/", post(create))
.route("/", get(get_all))
.route("/:id", get(get_by_uuid))
.route("/:id", patch(update))
.route("/:id", delete(delete_book))
.route("/test/:id", get(test_sqlx_query));
routes
}
Handlers
- Similar a un service
- Lógica de negocio, como los métodos CRUD
Structs
- Similar a los DTOs o esquemas