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 .

Desenvolvedor do MongoDB
Central de desenvolvedor do MongoDBchevron-right
Idiomaschevron-right
Pythonchevron-right

Crie uma API RESTful com Flask, MongoDB e Python

Mark Smith10 min read • Published Jan 14, 2022 • Updated Sep 11, 2024
FlaskPython
Ícone do FacebookÍcone do Twitterícone do linkedin
Avalie esse Tutorial
star-empty
star-empty
star-empty
star-empty
star-empty
Esta é a primeira parte de uma pequena série de publicações no blog chamada "Rewrite it in Rust (RiiR)" (Reescreva em Rust, em português). É um título irônico para algumas postagens que investigarão as semelhanças e diferenças entre o mesmo serviço escrito em Python com Flask, e Rust com Actix-Web.
Esta publicação mostra como criei uma API RESTful para uma coleção de receitas de coquetéis que, por acaso, tenho aqui comigo. O objetivo é mostrar um servidor de API com alguma complexidade, portanto, embora seja um exemplo pequeno, ele abordará fatores importantes como:
  • Transformação de dados entre o banco de dados e uma representação JSON.
  • Validação de dados.
  • Paginação.
  • Tratamento de erros.

Pré-requisitos

  • Python 3.8 ou superior
  • Um MongoDB Atlas cluster. Siga o guia "Introdução ao Atlas" para criar sua conta e o MongoDB cluster. Anote o nome de usuário, a senha e string de conexão do banco de dados, pois você precisará deles mais tarde.
Este é um guia avançado, então ele abordará um grupo de bibliotecas diferentes que podem ser reunidas para criar um servidor declarativo de API Restful sobre o MongoDB. Não cobrirá padrões repetidos na base de código, então se você quiser construir a coisa toda, recomendo verificar o código fonte, que está todo no GitHub.
Ele não abordará os conceitos básicos de Python, Flask ou MongoDB, então, se é isso que você está procurando, recomendo conferir os seguintes recursos antes de começar esta publicação:

Comece a usar

Comece clonando a fonte do código de amostra do GitHub. Há quatro diretórios de nível superior:
  • actix-cocktail-api: você pode ignorar isso por enquanto.
  • data: contém uma exportação dos dados do meu coquetel. Você importará esses dados para seu cluster em breve.
  • flask-cocktail-api: o código para esta publicação no blog.
  • test_scripts: alguns scripts de shell que usam curl para testar a interface HTTP do servidor da API.
Há mais detalhes no repositório do GitHub, mas o básico é: instale o projeto com seu virtualenv ativo:
1pip install -e .
Em seguida, você deve importar os dados para o seu cluster. Defina a variável de ambiente $MONGO_URI como seu URI de cluster. Essa variável de ambiente será usada em breve para importar seus dados e também pelo aplicativo Flask. Eu uso direnv para configurar isso e coloco a seguinte linha no meu arquivo .envrc no diretório do meu projeto:
1export MONGO_URI="mongodb+srv://USERNAME:PASSW0RD@cluster0-abcde.azure.mongodb.net/cocktails?retryWrites=true&w=majority"
Observe que seu banco de dados deve se chamar "cocktails," e a importação criará uma coleção chamada "recipes." Depois de verificar se $MONGO_URI está definido corretamente, execute o seguinte comando:
1mongoimport --uri "$MONGO_URI" --file ./recipes.json
Agora você poderá executar o aplicativo Flask no diretório flask-cocktail-api:
1FLASK_DEBUG=true FLASK_APP=cocktailapi flask run
(Você pode executar make run se preferir).
Verifique a saída para garantir que ela esteja de acordo com a configuração e, em seguida, em uma janela de terminal diferente, execute o script list_cocktails.sh no diretório test_scripts. Deve imprimir algo assim:
1{
2 "_links": {
3 "last": {
4 "href": "http://localhost:5000/cocktails/?page=5"
5 },
6 "next": {
7 "href": "http://localhost:5000/cocktails/?page=5"
8 },
9 "prev": {
10 "href": "http://localhost:5000/cocktails/?page=3"
11 },
12 "self": {
13 "href": "http://localhost:5000/cocktails/?page=4"
14 }
15 },
16 "recipes": [
17 {
18 "_id": "5f7daa198ec9dfb536781b0d",
19 "date_added": null,
20 "date_updated": null,
21 "ingredients": [
22 {
23 "name": "Light rum",
24 "quantity": {
25 "unit": "oz",
26 }
27 },
28 {
29 "name": "Grapefruit juice",
30 "quantity": {
31 "unit": "oz",
32 }
33 },
34 {
35 "name": "Bitters",
36 "quantity": {
37 "unit": "dash",
38 }
39 }
40 ],
41 "instructions": [
42 "Pour all of the ingredients into an old-fashioned glass almost filled with ice cubes",
43 "Stir well."
44 ],
45 "name": "Monkey Wrench",
46 "slug": "monkey-wrench"
47 },
48 ]
49 ...

Analisando tudo em detalhes

O código é dividido em três submódulos.
  • __init__.py contém todo o código de configuração do Flask e define todas as rotas HTTP.
  • model.py contém todas as definições do modelo Pydantic.
  • objectid.py contém uma definição de campo Pydantic que roubei do mapeador de dados de objetos para MongoDB Beanie.
Mencionei anteriormente que este código faz uso de várias bibliotecas:
  • O PyMongo e o Flask-PyMongo lidam com a conexão com o banco de dados. O Flask-PyMongo envolve especificamente o objeto da coleção do banco de dados para fornecer um método conveniente find_one_or_404.
  • Pydantic gerencia a validação de dados e alguns aspectos da transformação de dados entre o banco de dados e representações JSON.
  • junto com uma única função de FastAPI.

Validação e transformação de dados

Ao criar uma API robusta, é importante validar todos os dados que passam para o sistema. Seria possível fazer isso usando uma pilha de declarações if/else, mas é muito mais eficaz definir um esquema declarativamente e permitir que isso valide programaticamente os dados que estão sendo inseridos.
Usei uma técnica que aprendi com o Beanie, um ODM novo e bacana que, infelizmente, não pude usar na prática neste projeto, porque o Beanie é assíncrono e o Flask é um framework de bloqueio.
O Beanie usa Pydantic para definir um esquema e adiciona um tipo de campo personalizado para ObjectId.
1# model.py
2
3class Cocktail(BaseModel):
4 id: Optional[PydanticObjectId] = Field(None, alias="_id")
5 slug: str
6 name: str
7 ingredients: List[Ingredient]
8 instructions: List[str]
9 date_added: Optional[datetime]
10 date_updated: Optional[datetime]
11
12 def to_json(self):
13 return jsonable_encoder(self, exclude_none=True)
14
15 def to_bson(self):
16 data = self.dict(by_alias=True, exclude_none=True)
17 if data["_id"] is None:
18 data.pop("_id")
19 return data
Este esquema Cocktail define a estrutura de uma instância Cocktail, que será validada pelo Pydantic quando as instâncias forem criadas. Ele inclui outro esquema incorporado para Ingredient, que é definido de maneira semelhante.
Adicionei funções de conveniência para exportar os dados na instância Cocktail para um dict compatível com JSON ou um dict compatível com BSON. As diferenças são sucessivas, mas o BSON aceita tipos nativos ObjectId e datetime, por exemplo, enquanto ao codificar como JSON, é necessário codificar instâncias ObjectId de outra maneira (prefiro uma string contendo o valor hexadecimal do id), e objetos de data e hora são codificados como strings ISO8601.
O método to_json usa uma função importada do FastAPI, que recorre aos dados da instância, codificando todos os valores em um formato compatível com JSON. Ele já lida com instâncias datetime corretamente, mas para conseguir lidar com valores de ObjectId, extraí alguns códigos de campo personalizados do Beanie, que podem ser encontrados em objectid.py.
O método to_bson não precisa passar os dados dict por jsonable_encoder. Todos os tipos usados no esquema podem ser salvos diretamente com o PyMongo. É importante definir by_alias como True, de modo que a chave para _id seja apenas isso, o _id, e não o id do esquema sem um sublinhado.
1# objectid.py
2
3class PydanticObjectId(ObjectId):
4 """
5 ObjectId field. Compatible with Pydantic.
6 """
7
8 @classmethod
9 def __get_validators__(cls):
10 yield cls.validate
11
12 @classmethod
13 def validate(cls, v):
14 return PydanticObjectId(v)
15
16 @classmethod
17 def __modify_schema__(cls, field_schema: dict):
18 field_schema.update(
19 type="string",
20 examples=["5eb7cf5a86d9755df3a6c593", "5eb7cfb05e32e07750a1756a"],
21 )
22
23ENCODERS_BY_TYPE[PydanticObjectId] = str
Essa abordagem é perfeita para esse caso de uso específico, mas sinto que ela seria limitante em um sistema mais complexo. Existem muitos padrões para armazenar dados no MongoDB. Isso geralmente resulta no armazenamento de dados em um formato ideal para gravações ou leituras, mas não necessariamente a representação que você gostaria de exportar em uma API.
O que é um Slug?
Vendo o esquema acima, você deve ter se perguntado o que é "slug".
Um slug é um mnemônico exclusivo e seguro para URLs usado para identificar um documento. Aprendi a terminologia como desenvolvedor Django, em que esse termo faz parte do framework. Um slug geralmente é derivado de outro campo. Nesse caso, o slug é derivado do nome do coquetel, portanto, se um coquetel fosse chamado de "Rye Whiskey Old-Fashioned", o slug seria "rye-whiskey-old-fashioned".
Nessa API, esse cocktail pode ser acessado enviando uma solicitação GET para o endpoint /cocktails/rye-whiskey-old-fashioned.
Mantive o campo slug exclusivo separado do campo _id atribuído automaticamente , mas forneci ambos porque o slug poderia mudar se o nome do coquetel fosse ajustado e, nesse caso, o valor _id forneceria um identificador constante para procurar um documento exato.
Na versão Rust deste código, fui encorajado a usar uma abordagem diferente. É um pouco mais prolixa, mas, no final, fiquei convencido de que seria mais eficiente e flexível à medida que o sistema crescesse.

Criando um novo documento

Agora, mostrarei como seria um único ponto de extremidade, primeiro focando no ponto de extremidade "Criar", que lida com uma solicitação POST para /cocktails e cria um novo documento na coleção "receitas". Em seguida, ele retorna o documento que foi armazenado, incluindo o novo ID único que o MongoDB atribuiu como _id, porque essa é uma API RESTful e é isso que as APIs RESTful fazem.
1@app.route("/cocktails/", methods=["POST"])
2def new_cocktail():
3 raw_cocktail = request.get_json()
4 raw_cocktail["date_added"] = datetime.utcnow()
5
6 cocktail = Cocktail(**raw_cocktail)
7 insert_result = recipes.insert_one(cocktail.to_bson())
8 cocktail.id = PydanticObjectId(str(insert_result.inserted_id))
9 print(cocktail)
10
11 return cocktail.to_json()
Esse endpoint modifica diretamente o JSON de entrada para adicionar um item date_added com a hora atual. Em seguida, ele passa para o construtor do nosso esquema Pydantic. Neste ponto, se o esquema falhasse ao validar os dados, uma exceção seria gerada e exibida para o usuário.
Após validar os dados, to_bson() é chamado no Cocktail para convertê-lo em um dicionário compatível com BSON, e isso é passado diretamente para o método insert_one do PyMongo. Não há como fazer com que o PyMongo retorne o documento que acabou de ser inserido em uma única operação (embora um upsert usando find_one_and_update seja semelhante a isso).
Depois de inserir os dados, o código atualiza o objeto local com o id recém-atribuído e o retorna ao cliente.

Lendo um único coquetel

Graças a Flask-PyMongo, o endpoint para procurar um único coquetel é ainda mais simples:
1@app.route("/cocktails/<string:slug>", methods=["GET"])
2def get_cocktail(slug):
3 recipe = recipes.find_one_or_404({"slug": slug})
4 return Cocktail(**recipe).to_json()
Esse endpoint será abortado com um 404 se o slug não puder ser encontrado na coleção. Caso contrário, ele simplesmente instancia um Cocktail com o documento do banco de dados e chama to_json para convertê-lo em um dicionário que o Flask codificará automaticamente corretamente como JSON.

Listagem de todos os coquetéis

Esse ponto de extremidade é um monstro, e isso se deve à paginação e aos links para paginação. Nos dados de amostra acima, você provavelmente notou a seção _links:
1"_links": {
2 "last": {
3 "href": "http://localhost:5000/cocktails/?page=5"
4 },
5 "next": {
6 "href": "http://localhost:5000/cocktails/?page=5"
7 },
8 "prev": {
9 "href": "http://localhost:5000/cocktails/?page=3"
10 },
11 "self": {
12 "href": "http://localhost:5000/cocktails/?page=4"
13 }
14},
Esta seção _links é especificada como parte da especificação HAL (Hypertext Application Language). É uma boa ideia seguir um padrão para dados de paginação, e eu não estava com vontade de criar algo sozinho!
E aqui está o código para gerar tudo isso. Não entre em pânico.
1@app.route("/cocktails/")
2def list_cocktails():
3 """
4 GET a list of cocktail recipes.
5
6 The results are paginated using the `page` parameter.
7 """
8
9 page = int(request.args.get("page", 1))
10 per_page = 10 # A const value.
11
12 # For pagination, it's necessary to sort by name,
13 # then skip the number of docs that earlier pages would have displayed,
14 # and then to limit to the fixed page size, ``per_page``.
15 cursor = recipes.find().sort("name").skip(per_page * (page - 1)).limit(per_page)
16
17 cocktail_count = recipes.count_documents({})
18
19 links = {
20 "self": {"href": url_for(".list_cocktails", page=page, _external=True)},
21 "last": {
22 "href": url_for(
23 ".list_cocktails", page=(cocktail_count // per_page) + 1, _external=True
24 )
25 },
26 }
27 # Add a 'prev' link if it's not on the first page:
28 if page > 1:
29 links["prev"] = {
30 "href": url_for(".list_cocktails", page=page - 1, _external=True)
31 }
32 # Add a 'next' link if it's not on the last page:
33 if page - 1 < cocktail_count // per_page:
34 links["next"] = {
35 "href": url_for(".list_cocktails", page=page + 1, _external=True)
36 }
37
38 return {
39 "recipes": [Cocktail(**doc).to_json() for doc in cursor],
40 "_links": links,
41 }
Embora haja muito código, ele não é tão complexo quanto pode parecer à primeira vista. Duas solicitações são feitas ao MongoDB: uma para uma página de receitas de coquetéis e a outra para o número total de coquetéis na coleção. Vários cálculos são feitos para verificar quantos documentos devem ser ignorados e quantas páginas de coquetéis existem. Por fim, alguns links são adicionados para as páginas "anterior" e "próxima", se apropriado (ou seja, a página atual não é a primeira nem a última). A serialização dos documentos do coquetéis é feita da mesma forma que o endpoint anterior, mas desta vez em um loop.
Os endpoints atualizar e excluir são principalmente repetições do código que já incluí, então não vou incluí-los aqui. Confira-os no repositório GitHub se quiser ver como eles funcionam.

Tratamento de erros

Nada me irrita mais do que usar uma API JSON que retorna HTML quando ocorre um erro. Então, eu queria evitar que isso acontecesse.
Após o código de configuração do Flask e antes das definições de ponto de extremidade, o código registra dois manipuladores de erros:
1@app.errorhandler(404)
2def resource_not_found(e):
3 """
4 An error-handler to ensure that 404 errors are returned as JSON.
5 """
6 return jsonify(error=str(e)), 404
7
8
9@app.errorhandler(DuplicateKeyError)
10def resource_not_found(e):
11 """
12 An error-handler to ensure that MongoDB duplicate key errors are returned as JSON.
13 """
14 return jsonify(error=f"Duplicate key error."), 400
O primeiro manipulador de erros intercepta qualquer endpoint que falhe com um código de status 404 e garante que o erro seja retornado como um ditado JSON.
O segundo manipulador de erros intercepta um DuplicateKeyError gerado por qualquer ponto de extremidade e faz a mesma coisa que o primeiro manipulador de erros, mas define o código de status HTTP como "400 Solicitação inválida."
Enquanto escrevia esta publicação, percebi que tinha deixado passar um manipulador de erros para lidar com dados de Cocktail inválidos. Deixarei a implementação disso como um exercício para o leitor! De fato, essa é uma das dificuldades de escrever aplicativos Python robustos: como as exceções podem aparecer do fundo de sua pilha de dependências, é muito difícil prever de forma abrangente quais exceções seu aplicativo pode apresentar em diferentes circunstâncias.
Isso é algo muito diferente no Rust e, embora, como você verá, o tratamento de erros no Rust possa ser extenso e desafiador, comecei a adorá-lo por sua insistência na correção.

Encerrando

Quando comecei a escrever esta publicação, achei que seria relativamente simples. Como adicionei o requisito de que o código não deve ser apenas um exemplo de brinquedo, alguns dos desafios inerentes à criação de uma API robusta em qualquer banco de dados se tornaram aparentes.
Nesse caso, o Flask pode não ser a ferramenta certa para o trabalho. Escrevi recentemente uma publicação no blog sobre a criação de uma API com Beanie. Beanie e FastAPI são uma combinação perfeita para esse tipo de aplicativo e lidam com validação, transformação e paginação com muito menos código. Além disso, eles são autodocumentados e podem fornecer o esquema de dados em formatos abertos, incluindo OpenAPI spec e JSON schema!
Se você está prestes a criar uma API do zero, recomendo que você as verifique. Você pode ler as publicações de Aaron Bassett sobre a pilha FARM (FastAPI, React, MongoDB).
Publicarei em breve a segunda publicação desta série, Crie uma Cocktail API com Atix-Web, MongoDB e Rust, e depois concluirei com uma terceira publicação, Reescrevi em Rust – Como ficou?, na qual avaliarei os pontos fortes e fracos dos dois experimentos.
Obrigado por ler. Fique atento às próximas publicações!
Se tiver dúvidas, acesse o site da nossa comunidade de desenvolvedores, no qual os engenheiros e a comunidade do MongoDB ajudarão você a desenvolver sua próxima grande ideia com o MongoDB.

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

Usando os embeddings mais recentes da OpenAI em um sistema RAG com o MongoDB


Jul 01, 2024 | 15 min read
Tutorial

Adote BLE: implementando sensores BLE com MCU Devkits


Apr 02, 2024 | 13 min read
Tutorial

Crie um site de reserva de propriedades com Starlette, MongoDB e Twilio


Sep 09, 2024 | 17 min read
Tutorial

Orquestrando o MongoDB e BigQuery para excelência em aprendizado de máquina com as bibliotecas PyMongoArrow e BigQuery Pandas


Feb 08, 2024 | 4 min read
Sumário