Saltar al contenido principal

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.rs o al /modulo/mod.rs local

Diagrama

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

  1. Crear archivo con las variables de entorno
  2. Crear .env.template
  3. Agregar .env al .gitignore y si existe, .dockerignore
  4. 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 en main.rs al inicializar el servidor
  • nest se 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.rs global
  • Debe llamarse /<nombre>/mod.rs para 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