Rust - Criando o projeto de estudos de backend com Axum

Vamos começar a nossa jornada criando um projeto simples em Rust usando o Axum para criar APIs. Nesta primeira parte, vou demonstrar como criar o projeto, instalar os componentes necessários, configurar a inicialização do servidor e as primeiras rotas. A ideia é, a cada semana, adicionar novas funcionalidades ao projeto, então certas decisões podem parecer estranhas em um primeiro momento, pois estarei utilizando uma forma mais didática.

Criando o projeto

Para criar o projeto, usaremos o Cargo, que é o gerenciador de pacotes do Rust. Para iniciar o projeto, execute o comando:

cargo new axum_api_project
cd axum_api_project

Instalando os pacotes necessários

Uma vez dentro do projeto, vamos instalar os pacotes com o cargo add:

cargo add axum
cargo add tokio --features full
cargo add tower
cargo add serde --features derive
cargo add serde_json

Crates utilizadas:

  • axum: Responsável por gerenciar a API.
  • tokio: Responsável por executar funções assíncronas. O parâmetro features full indica que queremos todos os componentes.
  • tower: Gerencia middlewares para a aplicação.
  • serde: Responsável pela serialização e desserialização de valores e estruturas. Neste momento, usaremos apenas o componente derive.
  • serde_json: Responsável por serializar e desserializar JSON.

Criando o projeto

No main.rs, primeiro precisamos importar os pacotes necessários:

use axum::{routing::{get, post}, Json, Router};
use serde::{Deserialize, Serialize};

Explicação do código:

  • Router: Responsável por definir e gerenciar as rotas da API.
  • routing::{get, post}: Importa os métodos para criar rotas do tipo GET e POST.
  • Json: Utilizado para converter automaticamente JSON em structs Rust (desserialização) e structs Rust em JSON (serialização).
  • Deserialize: Permite que uma estrutura Rust seja criada a partir de um JSON recebido.
  • Serialize: Permite que uma estrutura Rust seja convertida em JSON ao ser enviada como resposta.

Agora, configuramos o projeto para utilizar assíncronismo, marcando a função main com os parâmetros do tokio:

#[tokio::main]
fn main() {
    println!("Hello, world!");
}

Agora, vamos criar a função para a rota GET, que irá retornar a string Hello, World!:

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

Utilizamos async para especificar que a função é assíncrona. Em Rust, quando queremos criar textos fixos no código, eles não são interpretados como String, mas sim como &str. Como esse valor não pode ser alterado e sempre será o mesmo, utilizamos 'static para garantir que ele tenha um tempo de vida estático. Agora, vamos criar uma estrutura para utilizarmos no POST:

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

Aqui criamos uma struct simples com somente o parâmetro name. APIs não recebem e nem retornam structs, mas sim JSON. Assim, precisamos transformar (deserializar) uma String JSON em struct User ao receber uma requisição e, ao responder, transformar (serializar) uma struct User em JSON. Para isso, utilizamos #[derive(Serialize, Deserialize)]. Agora, precisamos criar a função que será responsável pelo POST:

async fn post_user(Json(user): Json<User>) -> Json<User> {
    Json(User {
        name: format!("Hello: {}", user.name),
    })
}

Assim como a outra, essa função também é assíncrona. O parâmetro de entrada Json(user) usa Json<T>, que facilita a conversão automática de JSON para a estrutura Rust (User). No retorno, utilizamos novamente Json<T> para serializar a struct User de volta para JSON. Dentro da função, criamos uma nova estrutura User, baseada nos valores recebidos, mas alterando o parâmetro name. A estrutura é retornada dentro de Json() para que seja serializada corretamente.

Configurando e executando o servidor

Agora vamos configurar e iniciar o servidor Axum:

#[tokio::main]
fn main() {
    let app = Router::new()
        .route("/", get(hello_world))
        .route("/", post(post_user));
    
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

Explicação do código:

  • let app = Router::new(): Criamos a configuração do projeto para trabalhar como uma API e utilizar o sistema de rotas do Axum.
  • .route("/", get(hello_world)).route("/", post(post_user)): Configuramos as rotas. Os métodos GET e POST podem ser acessados na raiz / da API. Aqui não há conflito de endereço, pois os verbos HTTP são diferentes.
  • let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(): Configuramos o listener assíncrono com Tokio. “0.0.0.0:3000” indica que o servidor aceitará conexões de qualquer IP na porta 3000.
  • axum::serve(listener, app).await.unwrap(): Iniciamos o servidor Axum utilizando as configurações de listener e routers.

Executando a API

Para executar a API, use:

cargo run

Para testar as rotas, você pode utilizar o cURL:

Testando a rota GET

curl -X 'GET' 'http://127.0.0.1:3000/'

Você deverá ver o retorno:

Hello, World!

Testando a rota POST

curl -X 'POST' 'http://127.0.0.1:3000/' -H 'Content-Type: application/json' -d '{"name": "Meu Nome"}'

Você deverá ver o retorno:

{"name":"Hello: Meu Nome"}

Próximos passos

Isso é o suficiente para você começar a entender sobre APIs com Rust e Axum. Na próxima semana, irei implementar o uso de variáveis de ambiente. Apesar de ser algo básico, é muito importante no desenvolvimento de uma API para guardar informações de configuração de forma padronizada.

Obrigado por seguir o conteúdo e fique com Deus!

🔗 Link para a postagem no Linkedin

🔗 Confira o código no GitHub.