Projeto FastAPI Minimo
Este artigo mostra como começar mesmo o menor projeto FastAPI da melhor maneira possível, com uma estrutura adequada, usando ambiente virtual, linting de código, integração contínua (GitHub Actions), controle de versão e testes automatizados. A partir daí, é possível expandir o projeto de acordo com as necessidades, usando-o para serverless, ciência de dados, REST API, ensino de programação, base para novos gabaritos e outros fins.
A principal diferença deste projeto em relação a outros modelos é que ele possui apenas o conjunto mínimo de funcionalidades e dependências para formar uma base sólida para outros projetos.
Introdução
A melhor maneira de começar um novo projeto é usar um gabarito (template). Isso economiza tempo e evita erros comuns de configuração já que o gabarito contém soluções testadas e aprovadas, baseadas na experiência de outros desenvolvedores.
Diferentes modelos de projeto estão disponíveis para FastAPI, alguns dos quais são listados na documentação do projeto. No entanto, pode ser difícil encontrar um modelo que atenda exatamente às suas necessidades. A situação mais comum é escolher o que mais se aproxima e então ajustá-lo, mas isso pode ser demorado e frustrante porque é complicado remover ou substituir parte das decisões de implementação pré-definidas.
Em vez de criar mais um modelo repleto de funcionalidades e dependências, este tutorial constrói um projeto mínimo em FastAPI que servirá como uma base inicial sólida para projetos mais complexos e específicos. Começaremos com uma aplicação "Hello World" elementar, que será aprimorada incrementalmente até atingir o estado ideal que servirá como modelo para projetos mínimos em FastAPI.
Aplicação Elementar
O projeto elementar de Hello World
em FastAPI é composto por um arquivo principal (main.py
) e um arquivo de teste (test_hello_world.py
).
Hello World Elementar ===================== hello_world ├── main.py └── test_hello_world.py
O arquivo main.py
contém:
from fastapi import FastAPI app = FastAPI() @app.get('/') def say_hello() -> dict[str, str]: return {'message': 'Hello World'}
Como não existe um comando específico em Python
ou do FastAPI
para executar a aplicação, é necessário usar um servidor web ASGI tal como o Hypercorn ou o Uvicorn. A instalação do FastAPI
e do hypercorn
em um ambiente virtual e a execução do comando do hypercorn
são apresentadas abaixo:
|
$ python -m venv .venv
|
|
$ source .venv/bin/activate
|
|
(.venv) $ pip install fastapi hypercorn
|
|
...
|
|
(.venv) $ hypercorn main:app
|
|
[...] [INFO] Running on http://127.0.0.1:8000 (CTRL + C to quit)
|
em que main:app
(linha 5) especifica o uso da variável app
dentro do módulo main.py
.
Acessando http://localhost:8000
através do httpie, temos o seguinte resultado:
$ http :8000 HTTP/1.1 200 content-length: 25 content-type: application/json date: ... server: hypercorn-h11 { "message": "Hello World" }
Você pode usar o curl
no lugar do httpie
, se preferir:
O arquivo de teste test_hello_world.py
contém:
from fastapi.testclient import TestClient from main import app client = TestClient(app) def test_say_hello() -> None: response = client.get('/') assert response.status_code == 200 assert response.json() == {'message': 'Hello World'}
Para executar os testes, você também precisa que pytest
e httpx
estejam instalados:
Em seguida, execute o comando:
(.venv) $ pytest ================================= test session starts ================================== platform linux -- Python 3.11.4, pytest-7.4.0, pluggy-1.2.0 rootdir: /tmp/hello_world plugins: anyio-3.7.1 collected 1 item test_hello.py . [100%] ================================== 1 passed in 0.74s ===================================
Esses dois arquivos são suficientes para um exemplo ilustrativo, mas não formam um projeto que pode ser usado em produção. Para isso, é necessário adicionar boas práticas de engenharia de software, o que será feito nas próximas seções.
Projeto Python Perfeito + FastAPI
O artigo Como começar um projeto Python perfeito apresenta a criação de uma base inicial para ser usada com qualquer projeto Python. Como todo projeto FastAPI também é um projeto Python, podemos aplicar no projeto FastAPI mínimo essa mesma estrutura e configuração. Assim, incorporamos automaticamente as seguintes características:
- Ambiente virtual para o projeto.
- Estrutura de diretórios com separação em diferentes módulos e arquivos.
- Linting de código usando ferramentas como
ruff
emypy
. - Testes automatizados usando frameworks como
pytest
, integrados ao controle de versão e aoGitHub Actions
para garantir que o código esteja funcionando corretamente.
A listagem abaixo apresenta lado a lado a estrutura do projeto Hello, World
antes (lado esquerdo) e depois (lado direito) da aplicação do gabarito do projeto Python perfeito
:
Hello World Elementar Hello World + Perfect Python Project ===================== ==================================== hello_world hello_world │ ├── .github │ │ └── workflows │ │ └── continuous_integration.yml │ ├── .hgignore │ ├── hello_world │ │ ├── __init__.py ├── main.py │ └── main.py │ ├── Makefile │ ├── poetry.lock │ ├── pyproject.toml │ ├── README.rst │ ├── scripts │ │ └── install_hooks.sh │ └── tests └── test_hello_world.py └── test_hello_world.py
Esta ainda não é a estrutura nem a configuração finais porque alguns ajustes relacionadas ao FastAPI
ainda são necessárias:
- Instalação de dependências específicas de uma aplicação
FastAPI
. - Reorganização do arquivo
main.py
. - Configuração da aplicação.
- Reorganização dos testes.
Vamos abordar cada uma dessas etapas nas seções seguintes.
Instalação das Dependências
As dependências que vieram do gabarito do projeto Python perfeito são relacionadas apenas a teste e linting. Ainda é necessário instalar dependências específicas que usaramos no projeto FastAPI mínimo:
$ poetry add fastapi=="*" hypercorn=="*" loguru=="*" python-dotenv=="*" uvloop=="*" $ poetry add --group=dev asgi-lifespan=="*" alt-pytest-asyncio=="*" httpx=="*"
O primeiro comando instala bibliotecas que são diretamente necessárias para o funcionamento da aplicação. O segundo instala as bibliotecas necessárias apenas para o desenvolvimento e teste da aplicação, mas que não são necessáriam nem devem ser instaladas em produção.
Bibliotecas principais:
- FastAPI
- Hypercorn é um servidor web ASGI
- Loguru é uma biblioteca que visa tornar o logging mais agradável
-
python-dotenv lê pares
nome=valor
de um arquivo.env
e define variáveis de ambiente correspondente -
uvloop é uma implementação de alta eficiência que substitui a solução padrão usada no
asyncio
Bibliotecas de desenvolvimento:
-
alt-pytest-asyncio é um plugin para
pytest
que possibilita o uso de fixtures e teste assíncronos - asgi-lifespan permite a execução de eventos de iniciação e término da aplicação (durante os testes), sem a necessidade de um servidor web ASGI
- httpx é uma biblioteca síncrona e assíncrona de HTTP
Reorganizando o Arquivo Principal main.py
O arquivo principal original contém apenas a declaração da única rota do projeto. Porém, o número de rotas tende a crescer com o tempo e o arquivo principal acabará se tornando ingerenciável.
É importante preparar o projeto para que possa evoluir de maneira organizada. Para isso, é recomendado declarar rotas, modelos, esquemas etc. em diretórios e arquivos específicos de cada abstração.
A estrutura de diretórios pode ser organizada por função ou entidade. A organização por função de um projeto em FastAPI contendo uma entidade user
ficaria:
. ├── models │ ├── __init__.py │ └── user.py ├── routers │ ├── __init__.py │ └── user.py └── schemas ├── __init__.py └── user.py
A outra opção é agrupar por entidade ao invés de função. Nesse caso, modelos, rotas e esquemas ficam dentro do diretório user
:
user ├── __init__.py ├── models.py ├── routers.py └── schemas.py
Usar uma ou outra estrutura é uma questão de preferência apenas. Eu prefiro usar a estrutura de agrupamento por função e não por entidade porque é mais fácil agrupar as importações em Python dessa forma.
Voltando ao projeto FastAPI mínimo, como só temos uma rota para hello world
e não há modelos ou esquemas a estrutura resultante de diretórios é:
routers ├── __init__.py └── hello.py
O arquivo hello.py
contém apenas a declaração da rota para /hello
que foi extraída de main.py
:
from fastapi import APIRouter from loguru import logger router = APIRouter() @router.get('/hello') async def hello_world() -> dict[str, str]: logger.info('Hello world!') return {'message': 'Hello World'}
main.py
Apesar de reduzido, o arquivo main.py
continua existindo na nova organização do projeto, mas com funcionalidade reduzida. Sua finalidade é apenas coordenar a criação da aplicação, o que engloba importar as rotas e incluir a função que controla os eventos de iniciação e término da aplicação (lifespan):
Os roteadores são importados (linha 5), agrupados (linhas 11 a 13) e depois incluídos na aplicação (linhas 14 e 15).
A linha 4 importa lifespan
do módulo resources
e que depois é usado na criação da aplicação (linha 9). lifespan
é um async context manager
que coordena os eventos de início e término do ciclo de vida da aplicação. Os detalhes sobre esse assunto serão tratados na próxima seção.
resources.py
Uma aplicação mais complexa precisará de recursos adicionais tais como conexões com banco de dados, cache, filas, etc. que precisam ser iniciados e encerrados de maneira adequada para que a aplicação funcione corretamente. Ainda que o projeto FastAPI mínimo não faça uso de nenhum recurso adicional, é importante que o código esteja preparado para quando for necessário no futuro. Assim, as funções que lidam com recursos adicionais ficarão concentradas no módulo resources.py
:
A função lifespan
(linha 10) é um async context manager que coordena as chamadas às funções para início (startup) e término (shutdown) da aplicação. A função startup
é chamada antes da aplicação iniciar e a função shutdown
é chamada depois que a aplicação termina. Estes são os momentos ideais para iniciar/encerrar conexões com outros serviços e alocar/desalocar recursos. Como o projeto FastAPI mínimo não faz uso de nenhum recurso adicional, as funções startup
e shutdown
basicamente contém marcações para as chamadas futuras (linhas 21 e 26).
A função startup
ainda faz uma chamada à função show_config
(linha 19) que exibe as variáveis de configuração em caso de DEBUG
(linhas 9, 19-26). Essa exibição é útil na depuração e testes.
Configuração
A configuração garante que a aplicação funcione corretamente em diferentes ambientes, como desenvolvimento, teste e produção. A fim de evitar que informações sensíveis tais como endereços e credenciais de acesso fiquem expostas no código-fonte do projeto, é recomendado que a configuração seja definida por meio de variáveis de ambiente.
Apesar dessa recomendação, é comum utilizar um arquivo chamado .env
para armazenar as configurações locais de ambientes de desenvolvimento e teste. Este arquivo evita a necessidade de redefinir manualmente as variáveis de ambiente em cada terminal, IDE ou após reiniciar o computador. Existem bibliotecas que identificam automaticamente o arquivo .env
e carregam as variáveis de ambiente definidas nele ao iniciar a execução do projeto. Porém, é importante configurar o controle de versão para que o arquivo .env
não seja rastreado.
config.py
O módulo config.py
é responsável por extrair as variáveis de ambiente e fazer verificações e outros ajustes necessários na configuração:
Na linha 5, load_dotenv
carrega configurações do arquivo .env
, se existir. Por padrão, load_dotenv
não sobrescreve variáveis de ambiente existentes.
Na linha 7, ENV
indica o tipo do ambiente em que o projeto está executando. Pode ser production
, development
ou testing
. Se nenhum valor for definido, o valor padrão é production
.
Na linha 13, DEBUG
indica se o projeto está em modo de desenvolvimento. De modo análogo, TESTING
indica se o projeto está em modo de teste (linha 14). DEBUG
costuma ser usado para influenciar o nível de detalhes de informações apresentadas durante a execução do projeto (LOG_LEVEL
), enquanto TESTING
costuma sinalizar quando executar algumas ações tais como criar uma massa de testes ou desfazer transações de banco de dados ao final de cada teste.
Na linha 17, LOG_LEVEL
indica o nível de log do projeto. Se não for definido na variável de ambiente, ou não estiver no modo de desenvolvimento, então o valor padrão é INFO
.
Na linha 18, os.environ['LOGURU_DEBUG_COLOR']
ajusta a cor de mensagens de log de nível DEBUG que será usado pelo loguru. É uma questão de preferência estética apenas. Não é essencial.
Testes
Os testes síncronos, como o usado no arquivo test_hello_world.py
, limitam significativamente a capecidade de testar aplicações baseadas em processamento assíncrono. Por exemplo, pode ser necessário realizar chamadas assíncronas durante os testes para confirmar se determinadas informações foram gravadas corretamente em um banco de dados após uma chamada de API.
Embora seja possível realizar chamadas assíncronas em testes ou funções síncronas, isso requer alguns malabarismos de programação ou o uso de bibliotecas adicionais. Por outro lado, esses problemas inexistem em testes assíncronos, já que chamar uma função síncrona em um contexto assíncrono é trivial.
Para adotar testes assíncronos, é necessário:
- instalar um plugin de testes assíncronos no
pytest
. Há três opções: pytest-asyncio, alt-pytest-asyncio e anyio. Vamos usaralt-pytest-asyncio
neste projeto porque resolve o problema e não requer nenhuma configuração adicional para ser usado. Nem mesmo é necessário marcar os testes compytest.mark.asyncio
. - substituir
TestClient
porhttpx.AsyncClient
como classe base para os testes
O teste assíncrono equivalente a test_hello_world.py
é:
from httpx import AsyncClient from hello_world.main import app async def test_say_hello() -> None: async with AsyncClient(app=app, base_url='http://test') as client: response = await client.get('/') assert response.status_code == 200 assert response.json() == {'message': 'Hello World'}
Como uma instância configurada de um AsyncClient
vai ser usada com frequência, vamos manter a sua criação centralizada em uma fixture no conftest.py e recebê-la como parâmetro em todos os testes em que for necessário.
Com a fixture, test_hello_world.py
fica:
from httpx import AsyncClient async def test_say_hello(client: AsyncClient) -> None: response = await client.get('/') assert response.status_code == 200 assert response.json() == {'message': 'Hello World'}
Com a reorganização da estrutura do projeto, test_hello_world.py
passa a ser tests/routes/test_hello.py
já que a estrutura de diretórios de testes reflete a estrutura de diretórios da aplicação.
conftest.py
O arquivo conftest.py
contém as fixture usadas nos testes:
A linha 9 garante que o projeto será executado no modo de teste. Note que essa linha deve estar antes da importação da aplicação (linha 11) para que outros módulos sejam configurados corretamente.
As linhas 14 à 17 definem a fixture app
que tem o objetivo de disparar os eventos de iniciação e término da aplicação. Esse disparo não acontece automaticamente de outra forma durante os testes, nem mesmo pelo context manager criado na fixture client
(linhas 20 a 23). Precisamos da biblioteca asgi-lifespan e da classe LifespanManager
(linha 16).
Tarefas Automatizadas Adicionais
Além das tarefas test
, lint
, format
e install_hooks
herdadas do gabarito do projeto Python perfeito
, vamos adicionar uma nova ação no arquivo Makefile
que torne mais fácil rodar a aplicação sem ter de lembrar os parâmetros do hypercorn
:
Para a linha de comando não ficar tão longa, parte dos parâmetros do hypercorn
fica em um arquivo de configuração de nome hypercorn.toml
:
Estrutura Final da Aplicação
A aplicação elementar inicial evoluiu primeiro absorvendo a estrutura do projeto Python perfeito e depois ainda foi alterada para ter uma estrutura mais adequada a uma aplicação FastAPI. A diferença entre a estrutura de diretórios anterior e a final é apresentada na listagem abaixo:
Hello World + Projeto Python Perfeito Projeto FastaAPI Mínimo ===================================== ======================= hello_world hello_world ├── .github ├── .github │ └── workflows │ └── workflows │ └── continuous_integration.yml │ └── continuous_integration.yml ├── .hgignore ├── .hgignore ├── hello_world ├── hello_world │ ├── __init__.py │ ├── __init__.py │ │ │ ├── config.py │ └── main.py │ ├── main.py │ │ ├── resources.py │ │ └── routers │ │ ├── __init__.py │ │ └── hello.py │ ├── hypercorn.toml ├── Makefile ├── Makefile ├── poetry.lock ├── poetry.lock ├── pyproject.toml ├── pyproject.toml ├── README.rst ├── README.rst ├── scripts ├── scripts │ └── install_hooks.sh │ └── install_hooks.sh └── tests └── tests ├── __init__.py ├── __init__.py └── test_hello_world.py │ ├── conftest.py └── routers ├── __init__.py └── test_hello.py
Considerações Finais
Durante a criação do projeto mínimo em FastAPI, algumas escolhas foram feitas baseadas nas minhas preferências pessoais. Por exemplo, a adoção de uvloop
para otimização e alt-pytest-asyncio
para permitir testes assíncronos. Mas como são poucas e pontuais, não comprometem nem o objetivo nem a extensibilidade do projeto mínimo.
O projeto mínimo em FastAPI, como o próprio nome indica, tem o objetivo de fornecer uma base para novos projetos, sejam eles serverless, de ciência de dados, que usam diferentes tipos de bancos de dados, para construir REST APIs e até para outros gabaritos.
Ao invés de digitar manualmente todo o código apresentado, use o gabarito disponibilizado no GitHub.
Para instanciar um novo projeto, é necessário usar o cookiecutter. Recomendo combiná-lo com pipx:
Próximo artigo: Empacotamento e Distribuição do Projeto FastAPI Mínimo
Artigo anterior: Como Começar um Projeto Python Perfeito
Comentários
Comments powered by Disqus