Criando uma API Rest com Rust

Nessa série, irei compartilhar os aprendizados que obtive construindo mais um projeto pessoal, hoje eu trago uma breve introdução sobre a construção APIs Rest com Rust

Escolhendo o framework

Rust possui grandes nomes no mercado de frameworks para construção de APIs rest: Rocket, Axum, Warp, Actix. Testei apenas as duas primeiras e acabei optando pelo Axum por uma melhor experiência, devido a um tempo de compilação menor.

Talvez em outros pontos o Rocket se sobressaia, talvez o seu tempo de compilação custoso seja um problema na minha máquina, mas ao precisar reiniciar a aplicação várias vezes durante o desenvolvimento, os 40 segundos do Rocket me pareceram muito desmotivadores. Por esse trecho da minha experiência, fui de Axum.

Hot reload

Uma funcionalidade que eu busco sempre que possível no meu ambiente de desenvolvimento é a de Hot Reload, com ela, cada vez que um arquivo é alterado e salvo a aplicação reinicia, assim, o ciclo de escrever-avaliar-refatorar código se torna extremamente rápido, para o Rust, temos o cargo watch, sugiro dar uma olhada na documentação para mais detalhes.

Hello, Axum

Começaremos iniciando um novo projeto com o Cargo

$ cargo new hello_world --bin

Adicione as seguintes dependências no Cargo.toml

[dependencies]
axum = "0.6"
tokio = { version = "1.22.0", features = ["full"] }
serde = { version = "1.0.149", features = ["derive"] }

Além do Axum, já mencionado, estamos adicionando o Tokio como nossa runtime async, e o Serde, para lidar com serialização e desserialização de JSON, caso seja a primeira vez que você vê esses termos, sugiro que veja os seguintes conteúdos assim que possível.

referencia tokio, async no rust e runtime

Com as dependências instaladas, editaremos o arquivo src/main.rs

// src/main.rs

use std::{error::Error, net::SocketAddr};

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

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
    let addr = SocketAddr::from(([127, 0, 0, 1], 8000));

	let app = Router::new().route("/", get(|| async { "Hello, Axum" }));

    println!("listening on {}", addr);

    axum::Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();

    Ok(())
}

Agora podemos executar nossa aplicação com cargo watch -x run, e ela irá responder a requisições GET no endpoint http://127.0.0.1:8000/

curl -X 'GET' \
  'http://127.0.0.1:8000/'
Hello, Axum

Na linha x registramos o endpoint GET /, o Axum denomina as funções que resolvem endpoints como handlers, handlers podem retornar qualquer estrutura que implemente a trait IntoResponse. Podemos começar o code split por aí, movendo a closure para uma função:

// add use axum::http::StatusCode;

pub async fn hello_axum() -> impl IntoResponse {
    (StatusCode::OK, "Hello, Axum")
}

Na linha x, colocamos o nosso handler:

// add use axum::http::StatusCode;

	let app = Router::new().route("/", get(hello_axum));

Documentação

Com um endpoint definido, é um bom momento para configurarmos a documentação da API. Nesse ponto, acredito que uma especificação como a OpenApi, junto do Swagger para a sua visualização seja um bom combo.

Para implementa-los, precisaremos de algumas bibliotecas, uma responsável pela especificação e outra pela sua apresentação, então vamos novamente ao arquivo Cargo.toml e adicionaremos utoipa e utoipa-swagger-ui como dependências:

[dependencies]
axum = "0.6"
tokio = { version = "1.22.0", features = ["full"] }
serde = { version = "1.0.149", features = ["derive"] }
utoipa = { features = ["axum_extras"], version = "2.4.2" }
utoipa-swagger-ui = { features = ["axum"], version = "3.0.1" }

E então, utilizamos o path attribute do Utoipa no nosso handler

#[utoipa::path(
    get,
    path = "/",
    responses(
        (status = 200, description = "Send a salute from Axum")
    )
)]
pub async fn hello_axum() -> impl IntoResponse {
    (StatusCode::OK, "Hello, Axum")
}

Uma API Rest pode conter inúmeros endpoint diferentes, precisamos de um ponto onde possamos unir todas essas especificações, para isso, o Utoipa fornece o derive macro OpenApi, e o adicionando a um struct, podemos armazenar todos os endpoints da nossa aplicação através do parametro paths do atributo #[openapi]:

// add use utoipa::OpenApi;

#[derive(OpenApi)]
#[openapi(paths(hello_axum))]
pub struct ApiDoc;

Feito isso, podemos expor a documentação em um endpoint com o Swagger-ui, e acoplá-la na nossa aplicação, através do médoto Router.merge:

// add use utoipa_swagger_ui::SwaggerUi;

let app = Router::new()
        .route("/", get(hello_axum))
        .merge(SwaggerUi::new("/swagger-ui").url("/api-doc/openapi.json", ApiDoc::openapi()));

E finalmente, se acessarmos http://127.0.0.1:8000/swagger-ui/, veremos a interface do Swagger:

swagger_ui.png

A partir do Utoipa, temos um leque de possibilidades, podemos definir o body e o header das requisições, os códigos HTTP a serem retornados, o formato das respostas e por aí vai, vale a pena separar um tempo na documentação da biblioteca para extrair o máximo do OpenApi.

O post se encerra por aqui, mas no próximo teremos algumas melhorias, como banco de dados e testes de integração.

Obrigado por ter lido até aqui, qualquer dúvida ou questionamento pode ser feito na seção de comentários, caso queira entrar em contato comigo mais facilmente, pode me mandar uma mensagem no Twitter, estou sempre disponível por lá.

Conteudo muito bom, se tiver mais algum conteudo compartilhar. Valei.

Seu conteúdo foi muito interessante, parabéns, estou ancioso para ver a continuação!