Rusty Full Stack

El blog para los amantes de Rust, Ionic y Vuejs

Jaime Blandón
Jaime Blandón Desarrollador de software desde 2009, entusiasta de Rust, Vuejs y Ionic!. Fundador de este blog, espero que las publicaciones te sean de utilidad y si tienes comentarios para mejorar, son bienvenidos.

Organizar Muchas Rutas Con Rust y Actix Web Con Scopes y ServiceConfig

Hola, espero te encuentres muy bien 😃

En los artículos anteriores, hemos estado realizando ejemplos de desarrollo web con Rust y actix-web, si has seguido los artículos, habrás notado que siempre hemos colocado nuestras rutas en nuestro archivo src/main.rs, y esto podría llevarte a la pregunta de qué pasará si nuestro proyecto es bastante.  En este artículo intentaré responder a una pregunta que he recibido en varias ocasiones y es la siguiente:

¿Cómo organizar las rutas si nuestro proyecto es bastante grande o comienza a crecer mucho?

Esta pregunta es muy válida y como te podrás imaginar, la respuesta es organizar nuestro código en diversos archivos, en los cuales podamos agrupar funciones, estructuras, librerías, etc. De esta forma, nuestro código será mucho más sencillo de mantener, leer y documentar.

En el blog tenemos una serie en la cual profundizamos la organización de código con Rust en módulos, librerías y diferentes archivos, puedes encontrar el inicio de la serie acá 😎

Organizar Código En Rust - Paquetes y Librerías.

Pero, ¿puede también organizarse las rutas de actix-web en diferentes archivos para organizarlo mejor? La respuesta la podemos encontrar en los Scopes y ServiceConfig! 💪

¿Qué son los Scopes en actix-web?

Los scopes en actix-web actúan como namespaces, o dicho de otra manera, agrupa bajo una ruta "padre" diferentes recursos o subrutas, un ejemplo es el siguiente. Imagina que tenemos estas rutas en nuestra aplicación con Rust y actix-web:

/api/usuarios

/api/notas

/api/login

Como puedes ver, en las tres rutas se repite /api, es decir, que los recursos (o endpoints) "usuarios", "notas" y "login" se encuentran bajo el Scope "api". Hay que mencionar que nuestra aplicación puede tener más de un Scope, por ejemplo nuestra aplicación se podría dividir en tres grande módulos:

  • El sitio web público (public)
  • Un api (api)
  • Una app privada (admin)

Y cada ruta se encontrarán bajo su scope el cual puede ser similar a:

/api -> subrutas

/public ->subrutas

/admin -> subrutas

🦀 Por cierto, si te interesa aprender sobre la creación de APIs con Rust y actix-web, recuerda que en el canal de youtube existe una serie al respecto, te dejo el enlace al inicio de la serie, no olvides suscribirte al canal y compartirlo 😉

Creación de API con Rusty y actix-web (Youtube).

Inclusive puedes tener scopes, dentro de otros scopes! Esto es bastante útil, si quieres organizar tus rutas en base a features, por ejemplo, si tienes un api con varios endpoints podrias tener un scope "padre" llamado /api y luego tener un módulo de usuarios con estas rutas:

GET /api/usuario

POST /api/usuario

PUT /api/usuario

DELETE /api/usuario.

En las rutas anteriores, el primer scope de nuestra app seria:

/app

y dentro de ese scope, tenemos otro llamado:

/usuario.

¿Qué es ServiceConfig en actix-web?

ServiceConfig o, de ahora en adelante, simplemente Config, es una estructura en actix-web, el cual nos permite organizar o modular recursos de forma separada que los recursos de otro Scope. Por ejemplo, imagina que tu aplicación tiene una sección pública, como por ejemplo, el sitio web de tu servicio, probablemente en ese scope no necesites conectarte a una base de datos, o a lo mejor sí, pero, tu área privada de tu aplicación, o por ejemplo, el área al cual le das acceso a un usuario por una suscripción, a lo mejor necesite conectarse a otra base de datos, entonces, Config, nos permite poder separar ambas conexiones sin que ellas sepan entre sí, que esa configuración existe.

Por el momento, no vamos a detallar mucho sobre los recursos de bases de datos pues aún no hemos creado ningún artículo sobre integración de una base de datos con actix-web (pero pronto tendremos contenido sobre ello 🥳). En lo que si nos vamos a enfocar, es en utilizar ServiceConfig para "agrupar" las rutas que luego utilizaremos en nuestros scopes.

Para comprender mejor estos conceptos y aprender a organizar mejor nuestras rutas en actix-web, comencemos a trabajar en nuestro ejemplo 

Organizar rutas en actix-web.

Vamos a hacer un ejemplo bastante simple, en el cual vamos a crear dos módulos en archivos separados los cuales agruparán sus propias rutas.

Nota: por simplicidad y para aprender de una forma sencilla sobre Scope y ServiceConfig, únicamente crearemos dos archivos y tanto las rutas como las funcionalidades vivirán en esos archivos, recuerda que es buena práctica organizar tu código con distintos patrones de diseño, uno muy popular es el MVC. Recuerda echar un vistazo a nuestro artículo sobre módulos en diferentes archivos con Rust, para que puedas darte una idea de cómo organizar mejor el código en Rust.

Organizar código en diferentes archivos con Rust.

Nuestro ejemplo simulará que estamos creando una app web el cual contiene tres Scopes:

1. El front page o página inicial de nuestro sitio web, el cual vivirá dentro de src/main.rs

2. Una sección de "administración" y el cual vivirá dentro del archivo src/dashboard.rs

3. Un API, el cual vivirá dentro del archivo src/api.rs (recuerda nuestra serie en youtube 😋)

Las rutas serán:

src/main.rs:

/

src/api.rs:

/api/usuarios

/api/notas

src/admin.rs:

/admin

/admin/login

Recuerda que puedes ver el código completo desde el repositorio en github dando click acá.

Creemos nuestro proyecto, el cual llamaremos organizar_rutas, en una terminal, dentro del folder de tu preferencia ejecuta los comandos:

cargo new organizar_rutas

cd organizar_rutas

Ahora vamos a agregar actix-web dentro de nuestras dependencias. puedes hacerlo utilizando "cargo add" o en el archivo Cargo.toml, puedes colocar el siguiente código:

        
        
            // Cargo.toml

[package]
name = "test-multiple-routes"
version = "0.1.0"
edition = "2021"

[dependencies]
actix-web = "4.6.0"

        
        
    

Dentro de nuestro folder "src" vamos a crear los archivos:

  • admin.rs
  • api.rs

Nuestro folder "src" debe verse de la siguiente forma:

Dentro de nuestro archivo src/main.rs vamos a crear un servidor web que, por el momento, solo escuchará en el puerto 8080, pero no tiene rutas configuradas. Modifica tu archivo src/main.rs para tener el siguiente código:

        
        
            // src/main.rs

use actix_web::{App, HttpServer};

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| App::new())
        .bind(("127.0.0.1", 8080))?
        .run()
        .await
}

        
        
    

Nuestro código anterior, es únicamente un boilerplate, por el momento el servidor web solo escucha, pero no sabe que hacer ya que no hemos configurado las rutas.

Ahora vamos a aprovechar de Config y Scopes 😉. Comenzaremos creando las rutas de nuestro "api". Para ello, vamos a simular dos rutas:

/api/usuarios

El cual simulará que devolvemos una respuesta en formato JSON similar a la siguiente:

        
        
            
[
    {
        "id": 1,
        "nombre": "Rusty"
    },
    {
        "id": 2,
        "nombre": "Full Stack"
    }
]
        
        
    

Y la otra ruta será:

/api/notas

La cual simulará que devolvemos una respuesta en formato JSON similar a:

        
        
            
[
    {
        "id": 1,
        "contenido": "Nota 1"
    },
    {
        "id": 2,
        "contenido": "Nota 2"
    }
]
        
        
    

Puesto que no es el objetivo de este artículo explicar sobre la creación de API, no nos detendremos mucho el la lógica de los endpoints, pero, si quieres aprender más a detalle sobre respuestas en formato Json con Rust y actix-web, te invito a ver este video (y suscribirte al canal 😬)

Crear API Con Rust - Respuestas en Formato Json.

Recuerda que hemos creado un archivo src/api.rs, el cual contendrá nuestras rutas referentes a nuesto "API". Dentro de src/api.rs coloca el siguiente código:

        
        
            // src/api.rs

use actix_web::{
    get,
    http::header::{self, ContentType},
    web, HttpResponse,
};

#[get("/usuarios")]
async fn get_usuarios() -> HttpResponse {
    HttpResponse::Ok()
        .content_type(header::ContentType::json())
        .body(
            r#"
        [
            {
                "id": 1,
                "nombre": "Rusty"
            },
            {
                "id": 2,
                "nombre": "Full Stack"
            }
        ]
        "#,
        )
}

#[get("/notas")]
async fn get_notas() -> HttpResponse {
    HttpResponse::Ok().content_type(ContentType::json()).body(
        r#"
        [
            {
                "id": 1,
                "contenido": "Nota 1"
            },
            {
                "id": 2,
                "contenido": "Nota 2"
            }
        ]
        "#,
    )
}

        
        
    

El código anterior no hace más que agregar las funciones para devolver las respuestas que comentamos anteriormente, algunos puntos a destacar de este archivo:

Los "use" son tal cual los que hemos utilizado anteriormente, con excepción del HttpServer y App, puesto que esos únicamente los utilizamos en src/main.rs

        
        
            
use actix_web::{
    get,
    http::header::{self, ContentType},
    web, HttpResponse,
};
        
        
    

La definición de las funciones, son exactamente iguales a como lo hemos venido haciendo, incluso podemos utilizar los macros como get, post, put, etc!!

        
        
            
#[get("/usuarios")]
async fn get_usuarios() -> HttpResponse {
...
}

#[get("/notas")]
async fn get_notas() -> HttpResponse {
...
}
        
        
    

Sin embargo, quiero que notes que el macro:

#[get()]

No está agregando el prefijo "/api", sino, solamente el nombre del recurso; pero entonces, ¿dónde colocamos nuestras rutas y como asignamos el scope "api"? 🤔

Las rutas, vamos a agregarlas en nuestro Config, para luego darles un Scope, para ello completemos nuestro código en src/api.rs.

        
        
            // src/api.rs

use actix_web::{
    get,
    http::header::{self, ContentType},
    web, HttpResponse,
};

#[get("/usuarios")]
async fn get_usuarios() -> HttpResponse {
    HttpResponse::Ok()
        .content_type(header::ContentType::json())
        .body(
            r#"
        [
            {
                "id": 1,
                "nombre": "Rusty"
            },
            {
                "id": 2,
                "nombre": "Full Stack"
            }
        ]
        "#,
        )
}

#[get("/notas")]
async fn get_notas() -> HttpResponse {
    HttpResponse::Ok().content_type(ContentType::json()).body(
        r#"
        [
            {
                "id": 1,
                "contenido": "Nota 1"
            },
            {
                "id": 2,
                "contenido": "Nota 2"
            }
        ]
        "#,
    )
}

pub fn config(cfg: &mut web::ServiceConfig) {
    cfg.service(get_usuarios)
       .service(get_notas);
}

        
        
    

Lo nuevo que hemos agregado es únicamente esta función y es la que contiene nuestro config (ServiceConfig):

        
        
            
pub fn config(cfg: &mut web::ServiceConfig) {
    cfg.service(get_usuarios)
       .service(get_notas);
}
        
        
    

como puedes ver, es una función bastante sencilla, en la cual agregamos "service" a nuestra config (cfg) tal cual como lo hemos venido haciendo con App anteriormente. Ojo, no existe solamente "service", puedes incluso agregar app_data, route, hasta más Scopes adicionales!. Puedes ver la documentación completa de ServiceConfig dando click acá. Pero, en esencia, esa función está agrupando las subrutas:

.../usuarios

.../notas

Ahora vamos a ponerlas dentro de un contexto, para ello, regresamos a nuestro archivo src/main.rs y colocamos este código:

        
        
            // src/main.rs

use actix_web::{web, App, HttpServer};

mod api;

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| App::new().service(web::scope("/api").configure(api::config)))
        .bind(("127.0.0.1", 8080))?
        .run()
        .await
}

        
        
    

Lo primero a observar, es que estamos haciendo saber a nuestra app que existe un módulo llamado "api", con la línea:

mod api;

Luego,  este comando está agregando un scope "/api", es decir, que todo lo que se encuentre dentro de ese web::scope, se accederá mediante esa ruta padre, en este caso, estamos pasando las rutas de nuestra configuración:

        
        
            
App::new().service(web::scope("/api").configure(api::config)
        
        
    

Ahora podemos ejecutar nuestra app con el comando:

cargo run

o si tienes instalado carg-watch:

cargo watch -x run

Ahora con nuestro servidor corriendo, si probamos cualquiera de estas rutas en un navegador (o Postman), veremos los siguientes resultados:

http://localhost:8080/api/usuarios

http://localhost:8080/api/notas

Genial, ahora hemos logrado organizar rutas de mejor forma en un archivo a parte de src/main.rs 🥳

Para agregar más scopes y config, es bastante similar, por ejemplo, ahora agregaremos el Scope del panel de administración, que como hemos mencionado, será:

/admin

Vamos a hacerlo bastante rápido puesto que es muy parecido a lo que ya hemos hecho, solamente que está vez, el código, lo colocaremos en src/admin.rs:

        
        
            // src/admin.rs

use actix_web::{web, HttpResponse};

async fn login() -> HttpResponse {
    HttpResponse::Ok().body("Login Form")
}

async fn init() -> HttpResponse {
    HttpResponse::Ok().body("Hola Admin!")
}

pub fn config(cfg: &mut web::ServiceConfig) {
    cfg.route("login", web::get().to(login))
        .route("init", web::get().to(init));
}


        
        
    

Como puedes ver, en este caso las funciones son más simples y únicamente responden con un mensaje de texto, pero quisiera que notaras la función de config:

        
        
            
pub fn config(cfg: &mut web::ServiceConfig) {
    cfg.route("login", web::get().to(login))
        .route("init", web::get().to(init));
}
        
        
    

En este caso, en lugar de utilizar "service", estamos utilizando "route". Esto lo hice así, porque quería que vieras la versatilidad de ServiceConfig y que podemos utilizar varias formas de definir las rutas tal cual si de App se tratase. El config de este archivo genera dos rutas:

.../login

.../init

A los cuales debemos crearle su Scope, en nuestro archivo src/main.rs colocamos este código:

        
        
            // src/main.rs

use actix_web::{web, App, HttpServer};

mod admin;
mod api;

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .service(web::scope("/api").configure(api::config))
            .service(web::scope("admin").configure(admin::config))
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

        
        
    

Como puedes ver, ahora tenemos un nuevo scope "admin" y todas sus subrutas está definidas en la configuración del módulo admin.

        
        
            
App::new()
     .service(web::scope("/api").configure(api::config))
     .service(web::scope("admin").configure(admin::config))
        
        
    

Si corres de nuevo el servidor y pruebas las siguientes rutas en un navegador (o Postman), verás estos resultados:

http://localhost:8080/admin/login

http://localhost:8080/admin/init

También puedes probar los endpoints bajo el context "/api" y verás que siguen funcionando 😎

Hemos agregado 4 rutas a nuestro proyecto, pero si observas los archivos, el código se ve mucho mejor organizado, en especial nuestro archivo src/main.rs puesto que ya no estamos definiendo todas las rutas ahí, sino que mas bien las estamos organizando con Scope y Config.

¿Qué pasa si agregamos un nuevo recurso a alguno de los scopes?

En este caso simplemente agregamos la ruta a la configuración, incluso podemos agregar recursos con el mismo nombre, por ejemplo, podemos querer tener estas rutas:

/api/usuarios

/admin/usuarios

Ya tenemos el recurso "usuarios" en nuestro scope "api", lo que haremos ahora será agregarlo a nuestro archivo src/admin.rs

        
        
            // src/admin.rs

use actix_web::{web, HttpResponse};

async fn login() -> HttpResponse {
    HttpResponse::Ok().body("Login Form")
}

async fn init() -> HttpResponse {
    HttpResponse::Ok().body("Hola Admin!")
}

async fn usuarios() -> HttpResponse {
    HttpResponse::Ok().body("Lista de usuarios")
}

pub fn config(cfg: &mut web::ServiceConfig) {
    cfg.route("login", web::get().to(login))
        .route("init", web::get().to(init))
        .route("usuarios", web::get().to(usuarios));
}

        
        
    

Como puedes observar, hemos agregado una función que responde con el mensaje "Lista de usuarios"

        
        
            
async fn usuarios() -> HttpResponse {
    HttpResponse::Ok().body("Lista de usuarios")
}
        
        
    

Y Agregamos la ruta en la configuración del módulo:

        
        
            
cfg.route("login", web::get().to(login))
        .route("init", web::get().to(init))
        .route("usuarios", web::get().to(usuarios));
        
        
    

Ahora ya no debemos agregar nada más en nuestro archivo src/main.rs porque el Config del Scope "admin" se encarga de agregar la ruta por nosotros 😊

Si vuelves a ejecutar el servidor podrás observar que ahora puedes acceder a:

http://localhost:8080/admin/usuarios

Pero también esta url sigue funcionando como la hemos definido 👍:

http://localhost:8080/api/usuarios

Lo último que nos queda es agregar una ruta desde nuestro archivo src/main.rs, lo podríamos hacer simplemente con "service" o "route", pero este artículo es sobre Config y Scope, así que lo haremos así 😋

        
        
            // src/main.rs

use actix_web::{web, App, HttpResponse, HttpServer};

mod admin;
mod api;

async fn front_page() -> HttpResponse {
    HttpResponse::Ok().body("Mi Inicio!")
}

fn main_config(cfg: &mut web::ServiceConfig) {
    cfg.route("/", web::get().to(front_page));
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .configure(main_config)
            .service(web::scope("/api").configure(api::config))
            .service(web::scope("admin").configure(admin::config))
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

        
        
    

Como puedes ver, también a App, se le puede agregar su propia configuración:

        
        
            
App::new()
    .configure(main_config)
        
        
    

Quizá para una sola ruta no era necesario y tampoco era necesario hacer más grande el archivo src/main.rs, pero por motivos "didácticos" quería mostrarles ese feature de actix 😅.

Ahora vuelve a correr el servidor y podemos probar la ruta:

http://localhost:8080

Y veremos:

Te invito también a probar todas las rutas que hemos agregado a nuestra aplicación y verás que siguen funcionando perfectamente:

http://localhost:8080

http://localhost:8080/api/usuarios

http://localhost:8080/api/notas

http://localhost:8080/admin/login

http://localhost:8080/admin/init

http://localhost:8080/admin/usuarios

Hemos agregado un total de 6 rutas y al observar el código, verás que todo se ve mucho mejor organizado. En este ejemplo agregamos rutas con código bastante sencillo, sin embargo, en la vida real, a lo mejor te toque modular un poco más tu código siguiendo algún patrón de diseño como MVC, etc, sin embargo ya tenemos artículos en el blog en el que profundizamos sobre organización de código, puedes combinar ese contenido con el de este artículo y tener tu código mucho mejor organizado.

Recuerda que puedes ver el código completo desde el repositorio en github dando click acá.

Ahora hemos aprendido a organizar mejor nuestras rutas web con Rust y actix web 😁

Si esta publicación te ha sido de utilidad, compártela con tus amigos y en las redes sociales. No te olvides de seguirme en X (twitter) ni de suscribirte al canal de youtube.

Si esta información te ha sido de utilidad y te gustaría apoyar a la creación de más artículos y contenidos puedes convertirte en mecenas desde patreon te estaré sumamente agradecido 🦀

println!("Hasta la próxima!");


 Utilizamos cookies propias y de terceros para mejorar tu experiencia, mostrar publicidad y análisis de navegación, puedes encontrar el detalle en nuestra Política de Cookies