Rust - Organizando o projeto em módulos

Fala, pessoal! Vamos continuar nossa série sobre como criar uma API com Rust e Axum. Hoje, o foco será na organização do código. Escrever todo o código no arquivo main.rs não é recomendado, pois conforme o desenvolvimento avança, o arquivo pode ficar tão grande que se torna quase impossível de manter. O que faremos hoje é separar o código em módulos. Mas o que são módulos no Rust?

Os módulos em Rust são usados para organizar e estruturar o código, permitindo melhor reutilização e separação de responsabilidades. Eles ajudam a dividir um programa em partes menores e mais gerenciáveis.

Obs.: Faremos uma organização básica para demonstrar como estruturar os módulos. Conforme o projeto cresce, podemos ajustar a estrutura conforme a necessidade.

Existem várias formas de organizar os módulos. Hoje, vou mostrar a que mais utilizo: separando cada um por pastas e criando um arquivo mod.rs para definir a estrutura.

Estrutura de Arquivos e Diretórios

Primeiro, vamos criar a estrutura de arquivos e diretórios para os módulos, todos dentro do diretório src:

Crie o diretório config e dentro dele crie dois arquivos: config.rs e mod.rs.

Crie o diretório dtos e dentro dele crie dois arquivos: user_dto.rs e mod.rs.

Crie o diretório routes e dentro dele crie dois arquivos: routes.rs e mod.rs.

Sua estrutura de diretórios deve ficar assim:

src/
├── main.rs
├── config/
│   ├── config.rs
│   ├── mod.rs
├── dtos/
│   ├── mod.rs
│   ├── user_dto.rs
├── routes/
│   ├── mod.rs
│   ├── routes.rs

Configurando os Módulos

Dentro de src/config/config.rs, primeiro importe os módulos necessários e depois mova a struct e o impl do Config para o arquivo:

use std::env;

pub struct Config {
    pub my_rust_variable: String,
}

impl Config {
    pub fn new() -> Self {
        Config {
            my_rust_variable: env::var("MY_RUST_VARIABLE").expect("Variável não encontrada."),
        }
    }
}

Agora, configure o módulo. Dentro de src/config/mod.rs, insira:

pub mod config;

O Rust precisa do arquivo mod.rs para configurar os módulos. Sempre que você criar um módulo dentro de config, você deve inseri-lo no mod.rs utilizando o nome do arquivo sem a extensão .rs.

Dentro de src/dtos/user_dto.rs, primeiro importe os módulos necessários e depois mova a struct UserDto para o arquivo:

use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
pub struct UserDto {
    pub name: String,
}

Agora, configure o módulo. Dentro de src/dtos/mod.rs, insira:

pub mod user_dto;

Dentro de src/routes/routes.rs, primeiro importe os módulos e depois mova as funções das rotas para o arquivo:

use std::env;
use axum::Json;
use dotenvy::dotenv;
use crate::{config::config, dtos::user_dto};

pub async fn get_test() -> &'static str {
    "Hello, World!"
}

pub async fn get_system_env() -> String {
    match env::var("MY_SYSTEM_VARIABLE") {
        Ok(value) => value,
        Err(_) => String::from("Variável não encontrada."),
    }
}

pub async fn get_env() -> String {
    dotenv().ok();

    match env::var("MY_RUST_VARIABLE") {
        Ok(value) => value,
        Err(_) => String::from("Variável não encontrada."),
    }
}

pub async fn get_struct_env() -> String {
    dotenv().ok();
    let config = config::Config::new();
    config.my_rust_variable
}

pub async fn post_test(Json(user): Json<user_dto::UserDto>) -> Json<user_dto::UserDto> {
    Json(user_dto::UserDto { name: user.name })
}

Agora, configure o módulo. Dentro de src/routes/mod.rs, insira:

pub mod routes;

Modificando main.rs

Agora que separamos o nosso código em módulos, precisamos modificar o main.rs para podermos utilizá-los:

use axum::{routing::{get, post}, Router};

mod config;
mod dtos;
mod routes;

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/", get(routes::routes::get_test))
        .route("/system_env", get(routes::routes::get_system_env))
        .route("/env", get(routes::routes::get_env))
        .route("/struct_env", get(routes::routes::get_struct_env))
        .route("/", post(routes::routes::post_test));

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

Primeiro, importamos cada módulo. Depois, chamamos cada função a partir dele, como em routes::routes::get_system_env, deixando o código mais limpo e fácil de manter.

Com essa breve introdução a módulos, você já consegue organizar melhor o seu código. Aqui utilizamos uma separação básica de responsabilidades, então, neste momento, foque em aprender como utilizar os módulos em Rust. Depois, com o conceito bem fundamentado, você poderá aplicar Clean Code e outras técnicas para organizar ainda melhor seu código.

No próximo post, vamos falar sobre acesso a banco de dados! Vamos usar a crate SQLx para gerenciar conexões e queries, e eu vou te mostrar como criar um CRUD simples.

Valeu por acompanhar até aqui! Qualquer dúvida, deixa nos comentários. Fique com Deus!

🔗 Link para a postagem no Linkedin

🔗 Confira o código no GitHub.