Explore o novo chatbot do Developer Center! O MongoDB AI chatbot pode ser acessado na parte superior da sua navegação para responder a todas as suas perguntas sobre o MongoDB .

Junte-se a nós no Amazon Web Services re:Invent 2024! Saiba como usar o MongoDB para casos de uso de AI .
Desenvolvedor do MongoDB
Central de desenvolvedor do MongoDBchevron-right
Idiomaschevron-right
Rustchevron-right

Escrevendo uma API com MongoDB no Rust

Jacob Latonis10 min read • Published Jun 04, 2024 • Updated Jun 10, 2024
Rust
Ícone do FacebookÍcone do Twitterícone do linkedin
Avalie esse Tutorial
star-empty
star-empty
star-empty
star-empty
star-empty
Você escolheu escrever uma API que aproveita o MongoDB no Rust — gerenciamento de memória, tempo de vida, pool de banco de dados e muito mais! Você fez uma ótima decisão e estou ansioso para mostrar os fundamentos, o interesse e a facilidade com que você pode criar a API. Presumi que você sabe criar um projeto Rust com cargo e que está familiarizado com Cargo.toml e uso de dependência no Rust.

Introdução ao MongoDB (o caixote)

A caixa do MongoDB e sua documentação vêm totalmente detalhadas e ricas em recursos, prontas para uso (ou caixa)! A caixa do MongoDB está localizada em crates.io.
Para adicionar isso ao seu projeto, Go cargo add mongodb.
Você notará uma entrada adicionada a Cargo.toml que se parece um pouco com isto:
1[dependencies]
2mongodb = "2.8.1"
Depois que sua dependência do MongoDB for declarada, devemos extrair a documentação para mantê-la ao nosso lado enquanto iteramos esta API.
Além disso, há também uma entradadocs.rs que pode ser útil.

Escolhendo um tempo de execução

Ao usar o driver MongoDB para Rust, você precisa tomar uma decisão sobre qual tempo de execução usar. Isso se resume a duas opções: sync ou async.
Se você estiver desenvolvendo uma API, provavelmente desejará um tempo de execução assíncrono, que é o que usaremos neste tutorial. Por padrão, o driver Rust do MongoDB é fornecido com tokio como tempo de execução, mas ele pode ser configurado para usar async-std.
Existem casos de uso em que um tempo de execução de bloqueio (síncrono) pode ser útil, e o driver MongoDB Rust também fornece isso.
Embora estejamos usando o tempo de execução assíncrono, você pode escolher o tempo de execução de sincronização definindo-o na entradaCargo.toml:
1mongodb = {version = "2.8.1", features = ["sync"]}

Aproveitando o MongoDB Atlas

Antes de pularmos para o restante das dependências do nosso projeto, vamos configurar a dependência mais importante, se você ainda não tiver feito isso: uma instância do MongoDB! Para nossa sorte, podemos aproveitar o Atlas do MongoDB para criar nossa própria instância do MongoDB.
Vamos começar criando um novo projeto no MongoDB para essa API e o tutorial. Criar um projeto MongoDB Atlas
Depois de criar o projeto, vamos criar um sistema para hospedar nossa instância real do MongoDB. Criar uma implantação
Com uma implantação, podemos selecionar o nível e o provedor de hospedagem que gostaríamos de usar. O MongoDB oferece a camada M0 , que é perfeita para nosso uso de aprendizado e desenvolvimento inicial. Selecionando o nível gratuito
Depois de criar tudo isso e passar pelas etapas de segurança, vamos adicionar alguns dados à nossa instância do MongoDB. Forneci alguns dados de amostra no repositório do GitHub. O nome dele é bread_data.json.
Podemos aproveitar o recursoAdd Datado MongoDB Atlas para fazer isso: MongoDB Atlas' Add Data
Em seguida, Go para Import File. A partir daqui, aproveitaremos o MongoDB Compass. Usando Connect to Compass, o Atlas apresentará a você uma connection string parecida com esta: mongodb+srv://<user>:<password>@<db_name>.<identifier>.mongodb.net/. Usaremos isso para nos conectar ao Compass. Adicione opções de dados
Para se conectar à sua instância do MongoDB no Compass, insira a connection string quando solicitado: string de conexão
Depois de conectado no Compass, clique em + para adicionar um banco de dados à nossa instância: Adicionar banco de dados ao Compass
Nomeei meu banco de dadosbread e a coleção recipes. Novo banco de dados e coleção
Você verá que o Compass reconhece que não temos dados em nosso banco de dados e coleção recém-criados. Importar dados na coleção
Vamos importar alguns dados! Clique emImport Data e selecione o bread_data.json para carregar. Você verá uma pequena solicitação para que você saiba que a operação foi bem-sucedida: Importação bem-sucedida
Agora podemos girar de volta para o Atlas e ver nossos dados. Dados na coleção
Agora que temos nossa instância do MongoDB Atlas configurada e preenchida com dados, de volta às coisas do Rust!

Escolhendo uma caixa

Existem algumas estruturas web diferentes disponíveis no ecossistema Rust. Há actix-web, axum, foguete, warp e muito mais. À primeira vista, você pode pensar que todos são muito parecidos, e você está certo! Pode haver diferenças de desempenho entre eles ao comparar certas operações e similares, mas para o meu caso de uso (uma API de pequena a média escala para uso interno), poderíamos escolher qualquer uma delas e elas serviriam perfeitamente ao trabalho.
Creia em construir rapidamente e iterar muito em meus projetos. Sei que podemos ficar presos a milissegundos (ou até microssegundos) de diferença de desempenho, mas acho que, para a maioria dos aplicativos, você pode escolher a estrutura ou o projeto que mais lhe interessa e Go daí.
Para mim, a caixa que mais se destacou para mim foi Rocket. Meu principal motivo para escolher o Rocket foi a documentação, simplicidade e uma ótima experiência de desenvolvedor. Isso não quer dizer que as caixas mencionadas anteriormente (ou não mencionadas) não forneçam isso também, mas fui atraído por Rocket. Além disso, quando comecei no Rust, o Rocket foi uma das primeiras caixas que usei para criar uma API.

Introdução ao Rocket (o caixote)

Se você estiver interessado em criar uma API de alto desempenho com facilidade e rapidez, o Rocket nos permite fazer exatamente isso. Ele fornece exemplos de início rápido em seu repositório e, após algumas olhadas, pode-se começar a ter uma ideia de como a API é distribuída.
Go os fundamentos da configuração de uma API no Foguete.
Primeiro, vamos executar o cargo add -F json rocket para adicionar o Foguete às dependências do projeto. Isso resultará em algo como isto sendo adicionado em [dependencies]:
1rocket = {version = "0.5.0", features = ["json"]}

Pool de conexões do banco de dados

Enquanto adicionamos coisas à nossa lista de dependências, devemos adicionar uma caixa desenvolvida pela Rocket que nos permite fazer com que a Rocket use um wrapper para gerenciar um pool de collection para as conexões assíncronas feitas sem o cliente MongoDB e a caixa. Isso nos permitirá parametrizar nosso MongoDB database e collection e fazer com que cada função receba sua própria conexão para uso.
Para adicionar isso às nossas dependências, executamos cargo add -F mongodb rocket_db_pools, o que resultará em algo como isso sendo adicionado a [dependencies]:
1rocket_db_pools = { version = "0.1.0", features = ["mongodb"] }

Rocket.toml

Para configurar o Rocket para usar o nosso MongoDB database que criamos anteriormente no MongoDB Atlas, vá em frente e crie um novo arquivo de configuração e nomeie-o Rocket.toml. Esse é um arquivo de configuração que o Rocket lerá por padrão para pegar determinados itens de configuração. Para nossos propósitos, vamos definir nossa string de conexão do MongoDB como um campo chamado url. Para fazer isso, o conteúdo deRocket.toml deve ter um formato como este:
1[default.databases.<db_name>]
2url = "<MongoDB Connection String>"
Seguindo esse formato, meu Rocket.toml tem a seguinte aparência:
1[default.databases.db]
2url = "mongodb+srv://<username>:<password>@tutorial.xzpnmhw.mongodb.net/?retryWrites=true&w=majority&appName=tutorial"
Chamei o nome do meu pão porque todos os dados de teste que usei para essa API são receitas de pão. Eu os gerei aleatoriamente, então não há garantias de que sejam comestíveis! ;) Se quiser ler mais sobre o que é possível fazer com oRocket.toml, você pode encontrá-lo listado na documentação.

estrutura, estrutura

Antes de mergulharmos no código em si e começarmos a escrever a API, gostaria de analisar a estrutura da API em .rs arquivos.
Diagramação de cada arquivo e seu uso abaixo:
1.
2├── Cargo.lock # dependency info
3├── Cargo.toml # project and dependency info
4├── Rocket.toml # rocket config info
5└── src # directory where our rust code lives
6 ├── db.rs # file used to establish db
7 ├── main.rs # file used to start the API
8 ├── models.rs # file used for organization of data
9 └── routes.rs # file used for API routes

Conexão de banco de dados

Esse arquivo (db.rs) é usado para instanciar a conexão com nossa instância do MongoDB.
Você pode nomear a estrutura o que quiser. A parte importante aqui é usar o mesmo <db_name> que você definiu no Rocket.toml acima.
1use rocket_db_pools::{mongodb::Client, Database};
2
3#[derive(Database)]
4#[database("<db_name>")]
5pub struct MainDatabase(Client);
Na minha API, é definido assim. Observe o uso do nome “db, o mesmo que no meu Rocket.toml:
1use rocket_db_pools::{mongodb::Client, Database};
2
3#[derive(Database)]
4#[database("db")]
5pub struct MainDatabase(Client);
Há mais uma parte importante para inicializar a conexão com o MongoDB: Precisamos anexar a estrutura do banco de dados à nossa instância do Rocket. Em main.rs, precisamos inicializar o banco de dados e anexá-lo da forma abaixo:
1mod db;
2mod models;
3mod routes;
4
5use rocket::{launch, routes};
6use rocket_db_pools::Database;
7
8#[launch]
9fn rocket() -> _ {
10 rocket::build().attach(db::MainDatabase::init()).mount()
11}
Por enquanto, rust-analyzer pode relatar a falta de argumentos para mount(). Tudo bem — adicionaremos as rotas e tal mais tarde.

Modelos

Definir estruturas (modelos) consistentes e úteis para representar os dados que vão e vêm em nossa API é especialmente útil. Ele permite fazer suposições sobre o que os dados possuem, como podemos usá-los e assim por diante.
No meu arquivomodels.rs , defini uma estrutura que representa uma receita de pão e a nomeei, chocantemente, Recipe. Podemos habilitar as características de serialização e desserialização para nossa estrutura de e para JSON para facilitar a interatividade ao recuperar e enviar dados por meio da API. Isso é feito com a macro derivada: #[derive(Debug, Serialize, Deserialize)]. Também incluí a característicaDebugnele para que eu possa depurar e ver facilmente o conteúdo de uma entidade, se necessário.
1use mongodb::bson::oid::ObjectId;
2use rocket::serde::{Deserialize, Serialize};
3
4#[derive(Debug, Serialize, Deserialize)]
5#[serde(crate = "rocket::serde")]
6pub struct Recipe {
7 #[serde(rename = "_id", skip_serializing_if = "Option::is_none")]
8 pub id: Option<ObjectId>,
9 pub title: String,
10 pub ingredients: Vec<String>,
11 pub temperature: u32,
12 pub bake_time: u32,
13}

Roteamento

O roteamento é uma parte incrivelmente importante de uma API. O roteamento permite que o programa direcione a solicitação para o endpoint adequado para servir ou receber os dados. O arquivo routes.rs contém todas as rotas definidas na API. Abaixo está um exemplo de uma rota, mas o routes.rsreal precisará incluir suas importações e outros códigos.
1#[get("/recipes", format = "json")]
2pub async fn get_recipes(db: Connection<MainDatabase>) -> Json<Vec<Recipe>> {
3 let recipes: Cursor<Recipe> = db
4 .database("bread")
5 .collection("recipes")
6 .find(None, None)
7 .await
8 .expect("Failed to retrieve recipes");
9
10 Json(recipes.try_collect().await.unwrap())
11}
Não se esqueça de que precisamos adicionar as rotas à função de lançamento principal do Rocket.
Em main.rs, as rotas devem ser adicionadas da seguinte forma:
1mod db;
2mod models;
3mod routes;
4
5use rocket::{launch, routes};
6use rocket_db_pools::Database;
7
8#[launch]
9fn rocket() -> _ {
10 rocket::build().attach(db::MainDatabase::init()).mount(
11 "/",
12 routes![
13 routes::index,
14 routes::get_recipes,
15 routes::create_recipe,
16 routes::get_recipe
17 ],
18 )
19}

Handling

Agora que começamos a definir rotas para nossa API, também precisamos nos concentrar no tratamento de erros e nas respostas personalizadas, pois as coisas nem sempre saem conforme o planejado ao consultar, acessar ou criar dados!
Vamos modificar a rotaget_recipes()simples mostrada acima para evitar pânico e retornar códigos de status HTTP personalizados.
Para fazer isso, podemos aproveitar as estruturasstatus::Custom doRocket, que nos permitem especificar o código de status HTTP a ser retornado, bem como nossos dados. Para começar, modifique o tipo de retorno para status::Custom<Json<Value>>. Isso permite que o compilador do Rust saiba que retornaremos a estrutura de resposta HTTP personalizada doRocket, que inclui um código de status HTTP e um valor serde que podem ser serializados em JSON.
Iremos aproveitar a estrutura de resposta HTTP personalizada para cada uma das nossas rotas. Dessa forma, lidamos com quaisquer erros ou pânicos que possam surgir.

Operações CRUD

criar

Em nossa rotacreate_recipe(), só temos duas possibilidades, supondo que nossa instância do MongoDB esteja funcionando corretamente: a receita é criada com sucesso (retorno HTTP 201 [Criado]) ou a receita não pôde ser criada, indicando uma solicitação inválida (retornar HTTP 400 [Solicitação Inválida]).
1#[post("/recipes", data = "<data>", format = "json")]
2pub async fn create_recipe(
3 db: Connection<MainDatabase>,
4 data: Json<Recipe>,
5) -> status::Custom<Json<Value>> {
6 if let Ok(res) = db
7 .database("bread")
8 .collection::<Recipe>("recipes")
9 .insert_one(data.into_inner(), None)
10 .await
11 {
12 if let Some(id) = res.inserted_id.as_object_id() {
13 return status::Custom(
14 Status::Created,
15 Json(
16 json!({"status": "success", "message": format!("Recipe ({}) created successfully", id.to_string())}),
17 ),
18 );
19 }
20 }
21
22 status::Custom(
23 Status::BadRequest,
24 Json(json!({"status": "error", "message":"Recipe could not be created"})),
25 )
26}

Leia

A rota get_recipes()também tem apenas duas possibilidades realistas: retornar o vetor de receitas encontrado na instância do MongoDB ou retornar um vetor vazio, que não contém receitas (seja porque não há receitas na coleção ou porque a consulta falhou). Devido a esses resultados esperados, retornarei um tipoJson<Vec<Recipe>>, pois sempre retornaremos um vetor, mesmo que ele esteja vazio, com um status HTTP 200.
1#[get("/recipes", format = "json")]
2pub async fn get_recipes(db: Connection<MainDatabase>) -> Json<Vec<Recipe>> {
3 let recipes = db
4 .database("bread")
5 .collection("recipes")
6 .find(None, None)
7 .await;
8
9 if let Ok(r) = recipes {
10 if let Ok(collected) = r.try_collect::<Vec<Recipe>>().await {
11 return Json(collected);
12 }
13 }
14
15 return Json(vec![]);
16}
O get_recipe() é um pouco mais complicado, pois podemos encontrar alguns erros ou áreas diferentes que podem entrar em pânico. Precisamos levar em conta o seguinte: um document_idincorreto fornecido (retorno HTTP 400 [Solicitação incorreta]), uma receita encontrada por meio do document_id (retorno HTTP 200 [Ok]) ou uma receita que não está sendo encontrado por meio do document_id (retornar HTTP 404 [NotFound]).
Como temos inumeras possibilidades, estou retornando um tipo de status::Custom<Json<Value>>. Fornecerei um status de sucesso para a própria query no campo de status, uma mensagem se os dados não forem encontrados/atualizados e um campo de dados contendo os dados se eles forem encontrados.
1#[get("/recipes/<id>", format = "json")]
2pub async fn get_recipe(db: Connection<MainDatabase>, id: &str) -> status::Custom<Json<Value>> {
3 let b_id = ObjectId::parse_str(id);
4
5 if b_id.is_err() {
6 return status::Custom(
7 Status::BadRequest,
8 Json(json!({"status": "error", "message":"Recipe ID is invalid"})),
9 );
10 }
11
12 if let Ok(Some(recipe)) = db
13 .database("bread")
14 .collection::<Recipe>("recipes")
15 .find_one(doc! {"_id": b_id.unwrap()}, None)
16 .await
17 {
18 return status::Custom(
19 Status::Ok,
20 Json(json!({"status": "success", "data": recipe})),
21 );
22 }
23
24 return status::Custom(
25 Status::NotFound,
26 Json(json!({"status": "success", "message": "Recipe not found"})),
27 );
28}

Atualização

Existem algumas maneiras de abordar a atualização de registros por meio da API. Por meio do PUT, o usuário fornece o identificador e todo o documento de substituição.
1#[put("/recipes/<id>", data = "<data>", format = "json")]
2pub async fn update_recipe(
3 db: Connection<MainDatabase>,
4 data: Json<Recipe>,
5 id: &str,
6) -> status::Custom<Json<Value>> {
7 let b_id = ObjectId::parse_str(id);
8
9 if b_id.is_err() {
10 return status::Custom(
11 Status::BadRequest,
12 Json(json!({"status": "error", "message":"Recipe ID is invalid"})),
13 );
14 }
15
16 if let Ok(_) = db
17 .database("bread")
18 .collection::<Recipe>("recipes")
19 .update_one(
20 doc! {"_id": b_id.as_ref().unwrap()},
21 doc! {"$set": mongodb::bson::to_document(&data.into_inner()).unwrap()},
22 None,
23 )
24 .await
25 {
26 return status::Custom(
27 Status::Created,
28 Json(
29 json!({"status": "success", "message": format!("Recipe ({}) updated successfully", b_id.unwrap())}),
30 ),
31 );
32 };
33
34 status::Custom(
35 Status::BadRequest,
36 Json(
37 json!({"status": "success", "message": format!("Recipe ({}) could not be updated successfully", b_id.unwrap())}),
38 ),
39 )
40}

Excluir

Nem toda API será usada para o método DELETE e para excluir entidades do banco de dados, mas eu queria incluí-la para a posteridade e mostrar como implementá-la com o Rocket. Essa implementação pressupõe que se conheça o identificador BSON do objeto, mas você pode criar a rota e a função para excluir como achar melhor.
1#[delete("/recipes/<id>")]
2pub async fn delete_recipe(db: Connection<MainDatabase>, id: &str) -> status::Custom<Json<Value>> {
3 let b_id = ObjectId::parse_str(id);
4
5 if b_id.is_err() {
6 return status::Custom(
7 Status::BadRequest,
8 Json(json!({"status": "error", "message":"Recipe ID is invalid"})),
9 );
10 }
11
12 if db
13 .database("bread")
14 .collection::<Recipe>("recipes")
15 .delete_one(doc! {"_id": b_id.as_ref().unwrap()}, None)
16 .await
17 .is_err()
18 {
19 return status::Custom(
20 Status::BadRequest,
21 Json(
22 json!({"status": "error", "message":format!("Recipe ({}) could not be deleted", b_id.unwrap())}),
23 ),
24 );
25 };
26
27 status::Custom(
28 Status::Accepted,
29 Json(
30 json!({"status": "", "message": format!("Recipe ({}) successfully deleted", b_id.unwrap())}),
31 ),
32 )
33}

Resultado final

Testando a API

Se você quiser testar os endpoints por meio de curl ou wget, estou incluindo os seguintes comandos de curl para mostrar a funcionalidade da API. Analisaremos os testes completos da API em uma futura postagem do blog:).

Criar receita

1curl -v --header "Content-Type: application/json" --request POST --data '{"title":"simple bread recipe","ingredients":["water, flour"], "temperature": 250, "bake_time": 120}' http://localhost:8000/recipes

Atualizar receita

1curl -v --header "Content-Type: application/json" \
2 --request PUT --data '{"title":"new, updated title!","ingredients":["water", "flour", "salt", "sugar"], "temperature": 440, "bake_time": 60}' \
3 http://localhost:8000/recipes/<_id>

Obter receita

1curl -v --header "Content-Type: application/json" --header "Accept: application/json" http://localhost:8000/recipes/
2
3curl -v --header "Content-Type: application/json" --header "Accept: application/json" http://localhost:8000/recipes/<_id>

Excluir receita

1curl -v --header "Content-Type: application/json" --header "Accept: application/json" --request DELETE http://localhost:8000/recipes/<_id>
Se você estiver interessado em ver o resultado completo e o repositório de todo o código no total, poderá encontrar isso no meu GitHub. E se você tiver alguma dúvida ou quiser compartilhar seu trabalho, junta-se a nós na Comunidade de desenvolvedores do MongoDB.

Ícone do FacebookÍcone do Twitterícone do linkedin
Avalie esse Tutorial
star-empty
star-empty
star-empty
star-empty
star-empty
Relacionado
Artigo

Usando MongoDB com Rust Web Development Framework


Aug 29, 2024 | 1 min read
Artigo

Estruturando dados com Serde em Rust


Apr 23, 2024 | 5 min read
Artigo

Como o Prisma analisa um esquema de um banco de dados MongoDB


May 19, 2022 | 8 min read
Início rápido

Introdução ao Rust e MongoDB


Sep 23, 2022 | 17 min read
Sumário
  • Introdução ao MongoDB (o caixote)