Projeto Mínimo em FastAPI
Este artigo mostra a criação de uma projeto mínimo em FastAPI, basicamente um "Hello World" bem robusto. A ideia é 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í, você pode expandir o projeto do jeito que achar melhor, usando-o para serverless, ciência de dados, REST API, ensino de programação, outros gabaritos etc.
A principal diferença em relação a outros gabaritos é que este possui apenas o menor conjunto possível de funcionalidades de acordo com boas práticas de engenharia de software.
Introdução
Há vários gabaritos disponíveis para FastAPI e alguns são listados na própria documentação do projeto. Cada um deles representa uma opinião diferente de como é o projeto ideal e essa opinião se reflete nas funcionalidades oferecidas e na combinação de bibliotecas e frameworks adotados. Isso pode ser bom se o projeto que você quer criar se encaixa com o que o gabarito oferece. Por outro lado, é difícil encontrar um par perfeito entre suas necessidades e os gabaritos disponíveis. Mesmo encontrando um parecido, ainda é muito difícil customizá-lo removendo ou substituindo parte das suas decisões de implementação.
Ao invés de construir mais um gabarito repleto de preferências por determinadas funcionalidades e bibliotecas, vamos construir um projeto mínimo em FastAPI para servir de base para aplicações mais complexas e específicas. Para não haver distrações com funcionalidades, bibliotecas e frameworks adicionais e desnecessários, usamos uma aplicação "Hello World" como ponto de partida porque é a menor aplicação funcional possível em uma linguagem ou framework. Em seguida, faremos melhorias incrementais até chegar no estado que servirá de base para o gabarito de um projeto mínimo 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 rodar 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.10.7, pytest-7.2.0, pluggy-1.0.0 rootdir: /tmp/hello_world plugins: anyio-3.6.2 collected 1 item test_hello.py . [100%] ================================== 1 passed in 0.23s ===================================
Esses dois arquivos são suficientes para um exemplo ilustrativo, mas não formam um projeto que pode ser usado em produção. Vamos aprimorar o projeto nas próximas seções.
Projeto Python Perfeito + FastAPI
Como todo projeto FastAPI também é um projeto Python, dá para aproveitar toda a estrutura e configuração montada no artigo Como começar um projeto Python perfeito e aplicar no Hello World
em FastAPI. Isso resolve automaticamente os problemas de:
- Ambiente virtual
- Controle de versão
- Linting
- Testes automatizados
- Integração contínua
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. Outras mudanças ainda são necessárias para um projeto baseado em FastAPI:
- 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
Instalação das Dependências
As dependências que vieram do gabarito 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=="*" orjson=="*" python-dotenv=="*" uvloop=="*" $ poetry add --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
- orjson é uma biblioteca JSON de alta eficiência
-
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 crescer 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.
Como só temos uma rota para hello world
e não há modelos ou esquemas a estrutura resultante é:
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 com funcionalidade reduzida. Sua finalidade é coordenar a configuração da aplicação, o que engloba importar as rotas, ajustar algumas otimizações e incluir funções associadas aos eventos de iniciação e término da aplicação (startup e shutdown):
A classe ORJSONResponse
importada na linha 2 é uma subclasse de Response
. Ela fornece uma otimização na conversão de respostas no formato JSON
por ser baseada na biblioteca orjson. A linha 14 define ORJSONResponse como a classe padrão de resposta.
Os roteadores são importados (linha 6), agrupados (linhas 8 a 10) e depois incluídos na aplicação (linhas 18 e 19). À medida que novos roteadores são criados, basta manter o padrão de importação e agrupamento na lista routers
.
A principal diferença em relação ao arquivo principal é a configuração dos eventos de início e término (linhas 15 e 16), relacionados ao ciclo de vida de uma aplicação ASGI
(Lifespan Protocol). Esses eventos são acionados pelo servidor ASGI
ao instanciar e encerrar a aplicação e são os pontos ideais para criar/encerrar conexões com bancos de dados e outros serviços.
As funções startup
e shutdown
serão implementadas no módulo resources.py
para manter main.py
o mais enxuto possível.
resources.py
Este módulo concentra as funções que lidam com os eventos de iniciação e encerramento da execução da aplicação. Como este é um projeto mínimo FastAPI, ainda não temos conexões com bancos de dados e outros serviços mas os pontos de inserção das chamadas estão indicados no código:
A exibição das variáveis de configuração em caso de DEBUG
(linhas 9, 19-26) não é fundamental, mas é algo que costumo usar para ajudar na depuração e testes.
Configuração
Configuração é tudo o que precisa ser ajustado para que a aplicação funcione em um determinado ambiente (desenvolvimento, teste, produção etc.), tal como endereços e credenciais para acessar outros serviços. Segundo as recomendações do The Twelve-Factor App, a configuração deve ser obtida a partir de variáveis de ambiente ao invés de ser mantida diretamente no código-fonte do projeto.
Arquivo .env
Apesar da recomendação de não manter configuração em arquivos, é muito comum o uso de um arquivo .env
em ambientes locais de desenvolvimento e teste porque manter variáveis de ambiente não é muito prático: cada vez que você muda de terminal, fecha a IDE ou reinicia o computador, tem de definir todas as variáveis de ambiente novamente. Por outro lado, é muito cômodo manter as configurações locais no arquivo .env
e carregá-lo automaticamente durante a iniciação do projeto.
Esse padrão não viola os princípios do 12-factor-app
desde que o arquivo .env
não seja rastreado pelo controle de versão e, portanto, não faça parte do projeto oficial.
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
O maior problema de testes síncronos tal como test_hello_world.py
é que isso limita e dificulta testar uma aplicação baseada em processamento assíncrono. Por exemplo, se sua aplicação usa alguma biblioteca assíncrona de banco de dados tal como o Encode/Databases ou aioredis, você pode precisar fazer uma chamada assíncrona no teste para confirmar se uma determinada informação foi gravada corretamente no banco de dados depois de uma chamada a uma API.
Esses problemas simplesmente desaparecem se trocarmos testes sincronos por assíncronos. Afinal, fazer uma chamada síncrona em uma função assíncrona não requer nenhum esforço adicional.
Para a adoção de testes assíncronos será 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 freqê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 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 + Perfect Python Project Minimum FastaAPI Project ==================================== ======================== 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 orjson
e 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:
Comentários
Comments powered by Disqus