8 Melhores práticas para criar aplicativos FastAPI e MongoDB
Classifique este artigo
FastAPI é uma estrutura web moderna e de alto desempenho para construir APIs com Python 3.8 ou posterior, com base em dicas de tipo. Seu design se concentra em codificação rápida e redução de erros, graças à validação automática do modelo de dados e a menos códigos padrão. O suporte do FastAPI para programação assíncrona garante que as APIs sejam eficientes e escaláveis, enquanto os recursos de documentação integrados, como o MongoDB UI e o ReDoc, fornecem ferramentas interativas de pesquisa da API.
FastAPI seamlessly integrates with MongoDB through the Motor library, enabling asynchronous database interactions. This combination supports scalable applications by enhancing both the speed and flexibility of data handling with MongoDB. FastAPI and MongoDB together are ideal for creating applications that manage potentially large amounts of complex and diverse data efficiently. MongoDB is a proud sponsor of the FastAPI project, so you can tell it's a great choice for building applications with MongoDB.
All the techniques described in this article are available on GitHub — check out the source code! With that out of the way, now we can begin…
O FastAPI é particularmente adequado para criar APIs RESTful, em que solicitações de dados e atualizações no banco de dados são feitas usando solicitações HTTP, geralmente com cargas JSON. Mas a estrutura é igualmente excelente como back-end para sites HTML ou até mesmo aplicativos completos de página única (SPAs), onde a maioria das solicitações é feita via JavaScript. (Chamamos isso de pilha FARM — FastAPI, React, MongoDB — mas você pode trocar qualquer estrutura de componente front-end que desejar.) É particularmente flexível em relação ao back-end do banco de dados e à linguagem de modelo usada para renderizar HTML.
There are actually two Python drivers for MongoDB — PyMongo and Motor — but only one of them is suitable for use with FastAPI. Because FastAPI is built on top of ASGI and asyncio, you need to use Motor, which is compatible with asyncio. PyMongo is only for synchronous applications. Fortunately, just like PyMongo, Motor is developed and fully supported by MongoDB, so you can rely on it in production, just like you would with PyMongo.
Você pode instalá-lo executando o seguinte comando em seu terminal (recomendo configurar um ambiente virtual Python primeiro!):
1 pip install motor[srv]
The
srv
extra includes some extra dependencies that are necessary for connecting with MongoDB Atlas connection strings.Once installed, you'll need to use the
AsyncIOMotorClient
in the motor.motor_asyncio
package.1 from fastapi import FastAPI 2 from motor.motor_asyncio import AsyncIOMotorClient 3 4 app = FastAPI() 5 6 # Load the MongoDB connection string from the environment variable MONGODB_URI 7 CONNECTION_STRING = os.environ['MONGODB_URI'] 8 9 # Create a MongoDB client 10 client = AsyncIOMotorClient(CONNECTION_STRING)
Observe que a cadeia de conexão não é armazenada no código! O que me leva a...
It's very easy to accidentally commit secret credentials in your code and push them to relatively insecure places like shared Git repositories. I recommend making it a habit to never put any secret in your code.
When working on code, I keep my secrets in a file called
.envrc
— the contents get loaded into environment variables by a tool called direnv. Other tools for keeping sensitive credentials out of your code include envdir, a library like python-dotenv, and there are various tools like Honcho and Foreman. You should use whichever tool makes the most sense to you. Whether the file that keeps your secrets is called .env
or .envrc
or something else, you should add that filename to your global gitignore file so that it never gets added to any repository.In production, you should use a KMS (key management system) such as Vault, or perhaps the cloud-native KMS of whichever cloud you may be using to host your application. Some people even use a KMS to manage their secrets in development.
Embora eu tenha inicializado minha conexão de banco de dados no código acima no nível superior de um pequeno aplicativo FastAPI, é uma prática recomendada inicializar e fechar normalmente a conexão do cliente, respondendo a eventos de inicialização e desligamento em seu aplicativo FastAPI. Você também deve anexar seu cliente ao objeto de aplicativo do FastAPI para disponibilizá-lo às suas funções de operação de caminho, onde quer que elas estejam em sua base de código. (Outras estruturas às vezes se referem a elas como “routes” ou “endpoints.”. A FastAPI as chama de “path operations.”) Se você confiar em uma variável global, precisará se preocupar em importá-la em todos os lugares necessários, o que pode ser confuso.
O trecho de código abaixo mostra como responder ao início e ao desligamento do seu aplicativo e como lidar com o cliente em resposta a cada um desses eventos:
1 from contextlib import asynccontextmanager 2 from logging import info @asynccontextmanager 3 async def db_lifespan(app: FastAPI): 4 # Startup 5 app.mongodb_client = AsyncIOMotorClient(CONNECTION_STRING) 6 app.database = app.mongodb_client.get_default_database() 7 ping_response = await app.database.command("ping") 8 if int(ping_response["ok"]) != 1: 9 raise Exception("Problem connecting to database cluster.") 10 else: 11 info("Connected to database cluster.") 12 13 yield 14 15 # Shutdown 16 app.mongodb_client.close() 17 18 19 app: FastAPI = FastAPI(lifespan=db_lifespan)
Um ODM, ou mapeador de documentos de objetos, é uma biblioteca que converte entre documentos e objetos em seu código. É em grande parte análogo a um ORM no mundo dos bancos de dados RDBMS. O uso de um ODM é um tópico complexo e, às vezes, ele pode ocultar coisas importantes, como a forma como os dados são armazenados e atualizados no banco de dados ou até mesmo alguns recursos avançados do MongoDB que você pode querer aproveitar. Seja qual for o ODM que você escolher, você deve examiná-lo altamente para ter certeza de que ele fará o que você deseja e crescerá com você.
If you're choosing an ODM for your FastAPI application, definitely consider using a Pydantic-based ODM, such as ODMantic or Beanie. The reason you should prefer one of these libraries is that FastAPI is built with tight integration to Pydantic. This means that if your path operations return a Pydantic object, the schema will automatically be documented using OpenAPI (which used to be called Swagger), and FastAPI also provides nice API documentation under the path "/docs". As well as documenting your interface, it also provides validation of the data you're returning.
1 class Profile(Document): 2 """ 3 A profile for a single user as a Beanie Document. 4 5 Contains some useful information about a person. 6 """ 7 8 # Use a string for _id, instead of ObjectID: 9 id: Optional[str] = Field(default=None, description="MongoDB document ObjectID") 10 username: str 11 birthdate: datetime 12 website: List[str] 13 14 class Settings: 15 # The name of the collection to store these objects. 16 name = "profiles" 17 18 # A sample path operation to get a Profile: 19 20 async def get_profile(profile_id: str) -> Profile: 21 """ 22 Look up a single profile by ID. 23 """ 24 # This API endpoint demonstrates using Motor directly to look up a single 25 # profile by ID. 26 profile = await Profile.get(profile_id) 27 if profile is not None: 28 return profile 29 else: 30 raise HTTPException( 31 status_code=404, detail=f"No profile with id '{profile_id}'" 32 )
O objeto de perfil acima é documentado automaticamente no caminho "/docs":

If you feel that working directly with the Python MongoDB driver, Motor, makes more sense to you, I can tell you that it works very well for many large, complex MongoDB applications in production. If you still want the benefits of automated API documentation, you can document your schema in your code so that it will be picked up by FastAPI.
Como muitos aplicativos FastAPI incluem endpoints que fornecem dados JSON que são recuperados do MongoDB, é importante lembrar que determinados tipos que você pode armazenar em seu banco de dados, especialmente os tipos ObjectID e Binary, não existem no JSON. Felizmente, a FastAPI lida com datas e horários para você, codificando-os como strings formatadas.
Existem algumas maneiras diferentes de lidar com mapeamentos ObjectID. A primeira é evitá-los completamente usando um tipo compatível com JSON (como uma string) para valores de ID. Em muitos casos, isso não é prático, porque você já tem dados ou apenas porque o ObjectID é o tipo mais apropriado para sua chave primária. Nesse caso, você provavelmente desejará converter ObjectIDs em uma representação de string ao converter para JSON e fazer o inverso com dados que estão sendo enviados para seu aplicativo.
If you're using Beanie, it automatically assumes that the type of your _id is an ObjectID, and so will set the field type to PydanticObjectId, which will automatically handle this serialization mapping for you. You won't even need to declare the id in your model!
Se você especificar o tipo de resposta de suas operações de caminho, a FastAPI validará as respostas fornecidas e também filtrará todos os campos que não estejam definidos no tipo de resposta.
1 2 async def read_item(profile_id: str) -> Profile: 3 """ Use Beanie to look up a Profile. """ 4 profile = await Profile.get(profile_id) 5 return profile
Se você estiver usando o Motor, ainda poderá obter os benefícios de documentação, conversão, validação e filtragem retornando dados do documento, mas fornecendo o modelo Pydantic ao decorador:
1 2 3 4 5 async def read_item(profile_id: str) -> Mapping[str, Any]: 6 # This API endpoint demonstrates using Motor directly to look up a single 7 # profile by ID. 8 # 9 # It uses response_model (above) to tell FastAPI the schema of the data 10 # being returned, but it returns a dict directly, so that conversion and 11 # validation is done by FastAPI, meaning you don't have to copy values 12 # manually into a Profile before returning it. 13 profile = await app.profiles.find_one({"_id": profile_id}) 14 if profile is not None: 15 return profile
Um erro comum que as pessoas cometem ao construir servidores de API RESTful no MongoDB é armazenar os objetos de sua interface API exatamente da mesma maneira em seu MongoDB database. Isso pode funcionar muito bem em casos simples, especialmente se o aplicativo for uma API CRUD relativamente direta.
In many cases, however, you'll want to think about how to best model your data for efficient updates and retrieval and aid in maintaining referential integrity and reasonably sized indexes. This is a topic all of its own, so definitely check out the series of design pattern articles on the MongoDB website, and maybe consider doing the free Advanced Schema Design Patterns online course at MongoDB University. (There are lots of amazing free courses on many different topics at MongoDB University.)
Se estiver trabalhando com um modelo de dados diferente no banco de dados e no aplicativo, será necessário mapear os valores recuperados do banco de dados e os valores fornecidos por meio de solicitações às operações do caminho da API. Separar o modelo físico do modelo de negócios tem a vantagem de permitir que você altere o esquema do banco de dados sem necessariamente alterar o esquema da API (e vice-versa).
Even if you're not mapping data returned from the database (yet), providing a Pydantic class as the
response_model
for your path operation will convert, validate, document, and filter the fields of the BSON data you're returning, so it provides lots of value! Here's an example of using this technique in a FastAPI app:1 # A Pydantic class modelling the *response* schema. 2 class Profile(BaseModel): 3 """ 4 A profile for a single user. 5 """ 6 id: Optional[str] = Field( 7 default=None, description="MongoDB document ObjectID", alias="_id" 8 ) 9 username: str 10 residence: str 11 current_location: List[float] 12 13 # A path operation that returns a Profile object as JSON: 14 15 16 17 18 async def get_profile(profile_id: str) -> Mapping[str, Any]: 19 # Uses response_model (above) to tell FastAPI the schema of the data 20 # being returned, but it returns a dict directly, so that conversion and 21 # validation is done by FastAPI, meaning you don't have to copy values 22 # manually into a Profile before returning it. 23 profile = await app.profiles.find_one({"_id": profile_id}) 24 if profile is not None: 25 return profile # Return BSON document (Mapping). Conversion etc will be done automatically. 26 else: 27 raise HTTPException( 28 status_code=404, detail=f"No profile with id '{profile_id}'" 29 )
My amazing colleagues have built an app generator to do a lot of these things for you and help get you up and running as quickly as possible with a production-quality, dockerized FastAPI, React, and MongoDB service, backed by tests and continuous integration. You can check it out at the Full-Stack FastAPI MongoDB GitHub Repository.

Hopefully, this article has provided you with the knowledge to build a FastAPI application that will scale in both the amount of code you write, and the data you need to store. We're always looking for more tricks and tips for building applications with FastAPI and MongoDB, so if you have some suggestions, why not open a ticket on GitHub and we can have a chat?
We love to know what you're building with FastAPI or any other framework — whether it's a hobby project or an enterprise application that's going to change the world. Let us know what you're building at the MongoDB Community Forums. It's also a great place to stop by if you're having problems — someone on the forums can probably help you out!
Estou comentando apenas para apontar que o link para o Github parece não funcionar