Crie uma Cocktail API com o Beanie e o MongoDB
Avaliar este tutorial
Tenho uma coleção do MongoDB contendo receitas de coquetéis que criei durante a pandemia.
Recentemente, tenho tentado criar uma API com base nele, usando algumas tecnologias que conheço bem. Não fiquei muito feliz com os resultados. Escrever código para transformar o BSON que sai do MongoDB em JSON adequado é relativamente complicado. Eu senti que queria algo mais declarativo, mas minha tentativa mais recente — uma combinação de Flask, MongoEngine e Marshmallow— parecia desleixada e repetitiva. Eu estava prestes a começar a testar a criação do meu próprio framework declarativo e então me deparei com uma introdução a um novo ODM do MongoDB chamado Beanie. Parecia exatamente o que eu estava procurando.
O código usado nesta publicação se baseia muitona publicação do Beanie com link acima. Eu o personalizei de acordo com minhas necessidades e adicionei um endpoint extra que faz uso do MongoDB Atlas Search, para fornecer preenchimento automático, para uma GUI que estou planejando construir no futuro.
Tenho uma coleção de documentos que se parece um pouco com isto:
1 { 2 "_id": "5f7daa158ec9dfb536781b0a", 3 "name": "Hunter's Moon", 4 "ingredients": [ 5 { 6 "name": "Vermouth", 7 "quantity": { 8 "quantity": "25", 9 "unit": "ml" 10 } 11 }, 12 { 13 "name": "Maraschino Cherry", 14 "quantity": { 15 "quantity": "15", 16 "unit": "ml" 17 } 18 }, 19 { 20 "name": "Sugar Syrup", 21 "quantity": { 22 "quantity": "10", 23 "unit": "ml" 24 } 25 }, 26 { 27 "name": "Lemonade", 28 "quantity": { 29 "quantity": "100", 30 "unit": "ml" 31 } 32 }, 33 { 34 "name": "Blackberries", 35 "quantity": { 36 "quantity": "2", 37 "unit": null 38 } 39 } 40 ] 41 }
A promessa do Beanie e da FastAPI – apenas para criar um modelo para esses dados e fazer com que ele traduza automaticamente os tipos de campos complicados, como
ObjectId
e Date
entre a representação BSON e JSON – era muito tentadora, então eu chamei um novo projeto Python e defini meu esquema em um submódulo de modelos da seguinte forma:1 class Cocktail(Document): 2 class Settings: 3 name = "recipes" 4 5 name: str 6 ingredients: List["Ingredient"] 7 instructions: List[str] 8 9 10 class Ingredient(BaseModel): 11 name: str 12 quantity: Optional["IngredientQuantity"] 13 14 15 class IngredientQuantity(BaseModel): 16 quantity: Optional[str] 17 unit: Optional[str] 18 19 20 class IngredientAggregation(BaseModel): 21 """ A model for an ingredient count. """ 22 23 id: str = Field(None, alias="_id") 24 total: int
I was pleased to see that I could define a
Settings
inner class and override the collection name. It was a feature that I thought should be there, but wasn't totally sure it would be.A outra coisa que foi um pouco trabalhosa foi fazer
Cocktail
se referir a Ingredient
, que não estava definido até então. Felizmente, posso apenas usar o nome da turma em uma string e as coisas serão coladas mais tarde.O pacotebeanieCocktails, definido no arquivo
__init__.py
, contém principalmente código para inicializar FastAPI, Motor eBeanie:1 # ... some code skipped 2 3 async def app_lifespan(app: FastAPI): 4 # startup code goes here: 5 client: AsyncIOMotorClient = AsyncIOMotorClient( 6 Settings().mongodb_url, 7 ) 8 await init_beanie(client.get_default_database(), document_models=[Cocktail]) 9 app.include_router(cocktail_router, prefix="/v1") 10 11 yield 12 13 # shutdown code goes here: 14 client.close() 15 16 app = FastAPI(lifespan=app_lifespan) 17 18 # I'm using pydantic_settings to load the MongoDB connection string from an environment variable, 19 # or it defaults to a local installation. 20 class Settings(BaseSettings): 21 mongodb_url: str = "mongodb://localhost:27017/cocktails"
O código acima define um manipulador de vida útil para a inicialização do aplicativo FastAPI. Ele se conecta ao MongoDB, configura o Goanie com a conexão do banco de dados e fornece o modelo
Cocktail
que usarei para o Goanie.A última linha adiciona o
cocktail_router
ao FastAPI. É um APIRouter
definido no submódulo derotas .So now it's time to show you the routes file—this is where I spent most of my time. I was amazed by how quickly I could get API endpoints developed.
1 # ... imports skipped 2 3 cocktail_router = APIRouter()
O
cocktail_router
é responsável por rotear caminhos de URL para diferentes manipuladores de função que fornecerão dados a serem renderizados como JSON. Provavelmente, o manipulador mais simples é:1 2 async def list_cocktails(): 3 return await Cocktail.find_all().to_list()
Esse manipulador tira o máximo proveito desses fatos: a FastAPI renderizará automaticamente as instâncias do Pydantic como JSON; e os modelos Beanie
Document
são definidos usando o Pydantic.Cocktail.find_all()
retorna um iterador sobre todos os documentos Cocktail
na coleção recipes
. A FastAPI não pode lidar diretamente com esses iteradores, portanto, a sequência é convertida em uma lista usando o método to_list()
.1 just run
Caso contrário, você pode executá-lo diretamente:
1 uvicorn beaniecocktails:app --reload
E então você pode testar o endpoint apontando seu navegador para "http://localhost:8000/v1/cocktails/".

Um ponto de extremidade semelhante para apenas um único coquetel é perfeitamente encapsulado por dois métodos: um para pesquisar um documento por
_id
e gerar um erro "404 Não Encontrado" se ele não existir e um manipulador para rotear a solicitação HTTP. Os dois são perfeitamente colados usando a declaração Depends
que converte o cocktail_id
fornecido em uma instância Cocktail
carregada.1 async def get_cocktail(cocktail_id: PydanticObjectId) -> Cocktail: 2 """ Helper function to look up a cocktail by id """ 3 4 cocktail = await Cocktail.get(cocktail_id) 5 if cocktail is None: 6 raise HTTPException(status_code=404, detail="Cocktail not found") 7 return cocktail 8 9 10 async def get_cocktail_by_id(cocktail: Cocktail = Depends(get_cocktail)): 11 return cocktail
Now for the thing that I really like about Beanie: its integration with MongoDB's Framework de agregação. Aggregation pipelines can reshape documents through projection or grouping, and Beanie allows the resulting documents to be mapped to a Pydantic
BaseModel
subclass.Usando essa técnica, pode ser adicionado um ponto de extremidade que forneça um índice de todos os componentes e o número de coquetéis que cada um aparece em:
1 # models.py: 2 3 class IngredientAggregation(BaseModel): 4 """ A model for an ingredient count. """ 5 6 id: str = Field(None, alias="_id") 7 total: int 8 9 # routes.py: 10 11 12 async def list_ingredients(): 13 """ Group on each ingredient name and return a list of `IngredientAggregation`s. """ 14 15 return await Cocktail.aggregate( 16 aggregation_query=[ 17 {"$unwind": "$ingredients"}, 18 {"$group": {"_id": "$ingredients.name", "total": {"$sum": 1}}}, 19 {"$sort": {"_id": 1}}, 20 ], 21 projection_model=IngredientAggregation, 22 ).to_list()
1 [ 2 {"_id":"7-Up","total":1}, 3 {"_id":"Amaretto","total":2}, 4 {"_id":"Angostura Bitters","total":1}, 5 {"_id":"Apple schnapps","total":1}, 6 {"_id":"Applejack","total":1}, 7 {"_id":"Apricot brandy","total":1}, 8 {"_id":"Bailey","total":1}, 9 {"_id":"Baileys irish cream","total":1}, 10 {"_id":"Bitters","total":3}, 11 {"_id":"Blackberries","total":1}, 12 {"_id":"Blended whiskey","total":1}, 13 {"_id":"Bourbon","total":1}, 14 {"_id":"Bourbon Whiskey","total":1}, 15 {"_id":"Brandy","total":7}, 16 {"_id":"Butterscotch schnapps","total":1}, 17 ]
Gostei tanto desse recurso que decidi usá-lo junto com o MongoDB Atlas Search, que fornece pesquisa de texto gratuita em coleções do MongoDB, para implementar um endpoint de preenchimento automático.
A primeira etapa foi adicionar um índice do Atlas Search na collection
recipes
, na interface web do MongoDB Atlas :

Eu precisei adicionar o campo
name
como um tipo de campo "autocomplete".
Esperei que o índice terminasse de ser criado, o que não demorou muito, pois não é uma coleção muito grande. Então eu estava pronto para gravar meu endpoint de preenchimento automático:
1 2 async def cocktail_autocomplete(fragment: str): 3 """ Return an array of cocktail names matched from a string fragment. """ 4 5 return [ 6 c["name"] 7 for c in await Cocktail.aggregate( 8 aggregation_query=[ 9 { 10 "$search": { 11 "autocomplete": { 12 "query": fragment, 13 "path": "name", 14 } 15 } 16 } 17 ] 18 ).to_list() 19 ]
O estágio de agregação do
$search
utiliza especificamente um índice de pesquisa. Neste caso, estou usando o tipo autocomplete
, para corresponder ao tipo de índice que criei no campo name
. Como eu queria que a resposta fosse a mais leve possível, estou assumindo a serialização para JSON, extraindo o nome de cada instância Cocktail
e apenas retornando uma lista de strings.Os resultados são ótimos!
Abordando meu navegador em http://localhost:8000/v1/Cocktail_autocomplete?fragment=fi me dá
["Imperial Fizz","Vodka Fizz"]
e http://localhost:8000/v1/Cocktail_autocomplete?fragment= Ma me dá .
["Manhattan","Espresso Martini"]
A próxima etapa é criar um front-end do React, para que eu possa realmente chamar isso de aplicativo FARM Stack.
Fiquei realmente impressionado com a rapidez com que consegui colocar tudo isso em funcionamento. O tratamento de instâncias
ObjectId
era totalmente invisível, graças ao tipo PydanticObjectId
do Beanie, e vi outro código de exemplo que mostra como os valores BSON Date
são igualmente bem tratados.Preciso ver como posso criar alguma funcionalidade HATEOAS nos endpoints, com entidades vinculadas a suas URL canônicas. A paginação também é algo que será importante à medida que minha coleção crescer, mas acho que já sei como lidar com isso.
Espero que você tenha gostado dessa rápida apresentação da minha primeira experiência com o Beanie. Da próxima vez que estiver criando uma API sobre o MongoDB, recomendo que experimente!
Se esta foi sua primeira exposição à Estrutura de agregação, eu realmente recomendável que você leia nossa documentação sobre esse recurso poderoso do MongoDB. Ou, se você realmente quiser colocar a mão na massa, por que não conferir nosso cursogratuito de Agregação da MongoDB University?
Em caso de dúvidas, acesse nosso website da comunidade de desenvolvedores, no qual os engenheiros do MongoDB e a MongoDB Community ajudarão você a colocar em prática sua próxima grande ideia com o MongoDB.