Apresentando FARM Stack - FastAPI, React e MongoDB
Avalie esse Artigo
{Quando consegui minha primeira tarefa na área de programação, a pilha LAMP (Linux, Apache, MySQL, PHP)e suas variações- reinava suprema. Eu usava o WAMP no trabalho, o DAMP em casa e implementava nossos clientes no SAMP. Mas agora todas as pilhas com acrônimos memoráveis parecem ser muito avançadas em termos de JavaScript. MEAN (MongoDB, Express, Angular, Node.js), MERN (MongoDB, Express, React, Node.js), MEVN (MongoDB, Express, Vue, Node.js), JAM (JavaScript, APIs, Markup) e assim por diante.
Por mais que eu curta trabalhar com React e Vue, o Python ainda é minha linguagem favorita para criar web services de back-end. Eu queria os mesmos benefícios que obtive do MERN - MongoDB, velocidade, flexibilidade, código padrão mínimo - mas com Python em vez de Node.js. Com isso em mente, gostaria de apresentar a pilha FARM; FastAPI, React e MongoDB.
A pilha FARM é, em muitos aspectos, muito semelhante ao MERN. Mantivemos o MongoDB e o React, mas substituímos o back-end do Node.js e do Express por Python e FastAPI. FastAPI é um framework moderno e de alto desempenho em Python 3.6+ web. No que diz respeito aos frameworks web, é incrivelmente novo. O commit mais antigo do Git que eu consegui encontrar é de 5 de dezembro de 2018, mas é uma estrela em ascensão na comunidade Python. Ele já é usado em produção por empresas como Microsoft, Uber e Netflix.
E é rápido. Os benchmarks mostram que não é tão rápido quanto o Chi do Golang ou fasthttp, mas é mais rápido do que todos os outros frameworks Python testados e supera a maioria dos Node.js também.
Se você quiser experimentar a pilha FARM, criei um exemplo de aplicativo TODO que você pode clonar no GitHub.
1 git clone git@github.com:mongodb-developer/FARM-Intro.git
O código é organizado em dois diretórios: back-end e front-end. O código de back-end é o nosso servidor FastAPI. O código nesse diretório interage com nosso MongoDB database, cria nossos endpoints de API e, graças ao OAS3 (Especificação OpenAPI 3). Ele também gera nossa documentação interativa.
Antes de eu analisar o código, tente executar o servidor FastAPI. Você precisará de Python 3.8+ e um MongoDB database. Um Atlas Cluster gratuito será mais que suficiente. Anote seu nome de usuário, senha e string de conexão do MongoDB, pois você precisará deles em breve.
1 cd FARM-Intro/backend 2 pip install -r requirements.txt
1 export DEBUG_MODE=True 2 export DB_URL="mongodb+srv://<username>:<password>@<url>/<db>?retryWrites=true&w=majority" 3 export DB_NAME="farmstack"
Depois de ter tudo instalado e configurado, você pode executar o servidor com
python main.py
e acessar http://localhost:8000/docs no seu navegador.Essa documentação interativa é gerada automaticamente para nós pela FastAPI e é uma ótima maneira de experimentar sua API durante o desenvolvimento. Você pode ver que cobrimos os principais elementos do CRUD. Tente adicionar, atualizar e excluir algumas tarefas e explore as respostas que você recebe do servidor FastAPI.
Inicializamos o servidor em
main.py
; é aqui que criamos nosso aplicativo.1 app = FastAPI()
Anexe nossas rotas ou endpoints de API.
1 app.include_router(todo_router, tags=["tasks"], prefix="/task")
Inicie o loop de eventos assíncronos e o servidor ASGI.
1 if __name__ == "__main__": 2 uvicorn.run( 3 "main:app", 4 host=settings.HOST, 5 reload=settings.DEBUG_MODE, 6 port=settings.PORT, 7 )
E é também onde abrimos e fechamos a conexão com nosso servidor MongoDB.
1 2 async def startup_db_client(): 3 app.mongodb_client = AsyncIOMotorClient(settings.DB_URL) 4 app.mongodb = app.mongodb_client[settings.DB_NAME] 5 6 7 8 async def shutdown_db_client(): 9 app.mongodb_client.close()
Como a FastAPI é um framework assíncrono, estamos usando o Motor para nos conectar ao nosso servidor MongoDB. Motor é o driver Python assíncrono oficialmente mantido para o MongoDB.
Quando o evento de inicialização do aplicativo é acionado, abro uma conexão com o MongoDB e garanto que ele esteja disponível por meio do objeto do aplicativo para que eu possa acessá-lo posteriormente em meus diferentes roteadores.
Muitas pessoas pensam que o MongoDB não tem esquema, o que está errado. O MongoDB tem um esquema flexível. Ou seja, as coleções não impõem a estrutura do documento por padrão, então você tem a flexibilidade de fazer as escolhas de modelagem de dados que melhor atendem ao seu aplicativo e seus requisitos de desempenho. Portanto, não é incomum criar modelos ao trabalhar com um MongoDB database.
Os modelos do aplicativo TODO estão em
backend/apps/todo/models.py
, e são esses modelos que ajudam a FastAPI a criar a documentação interativa.1 class TaskModel(BaseModel): 2 id: str = Field(default_factory=uuid.uuid4, alias="_id") 3 name: str = Field(...) 4 completed: bool = False 5 6 class Config: 7 allow_population_by_field_name = True 8 schema_extra = { 9 "example": { 10 "id": "00010203-0405-0607-0809-0a0b0c0d0e0f", 11 "name": "My important task", 12 "completed": True, 13 } 14 }
Quero chamar a atenção para o campo
id
neste modelo. O MongoDB usa _id
, mas no Python, os sublinhados no início dos atributos têm um significado especial. Se você tiver um atributo em seu modelo que comece com um sublinhado, pydentic – a estrutura de validação de dados usada pelo FastAPI – assumirá que é uma variável privada, o que significa que você não poderá atribuir um valor a ele! Para contornar isso, nomeamos o campo id
, mas damos a ele um alias
de _id
. Você também precisa definir allow_population_by_field_name
como True
na classe Config
do modelo.Você pode notar que não estou usando os ObjectIds do MongoDB. Você pode usar ObjectIds com FastAPI; há apenas mais trabalho necessário durante a serialização e a desserialização. Ainda assim, para este exemplo, acho mais fácil gerar os UUIDs, então eles são sempre strings.
1 class UpdateTaskModel(BaseModel): 2 name: Optional[str] 3 completed: Optional[bool] 4 5 class Config: 6 schema_extra = { 7 "example": { 8 "name": "My important task", 9 "completed": True, 10 } 11 }
Quando os usuários estiverem atualizando tarefas, não queremos que eles alterem o id, portanto, o
UpdateTaskModel
inclui apenas os campos nome e concluído. Também tornei ambos os campos opcionais para que você possa atualizar qualquer um deles independentemente. Tornar ambos opcionais significou que todos os campos eram opcionais, o que me fez perder muito tempo decidindo como lidar com uma solicitação PUT
(uma atualização) em que o usuário não enviou nenhum campo para ser alterado. Veremos isso a seguir quando analisarmos os roteadores.Os roteadores de tarefas estão dentro de
backend/apps/todo/routers.py
.Para cobrir as diferentes operações CRUD (Criar, Ler, Atualizar e Excluir, em português), precisei dos seguintes endpoints:
- POST /task/ - cria uma nova tarefa.
- GET /task/ - visualizar todas as tarefas existentes.
- GET /task/{id}/ - visualizar uma única tarefa.
- PUT /task/{id}/ - atualizar uma tarefa.
- DELETE /task/{id}/ - excluir uma tarefa.
1 2 async def create_task(request: Request, task: TaskModel = Body(...)): 3 task = jsonable_encoder(task) 4 new_task = await request.app.mongodb["tasks"].insert_one(task) 5 created_task = await request.app.mongodb["tasks"].find_one( 6 {"_id": new_task.inserted_id} 7 ) 8 9 return JSONResponse(status_code=status.HTTP_201_CREATED, content=created_task)
O roteador create_task aceita os novos dados da tarefa no corpo da solicitação como uma string JSON. Gravamos esses dados no MongoDB e, em seguida, respondemos com um status de 201 HTTP e a tarefa recém-criada.
1 2 async def list_tasks(request: Request): 3 tasks = [] 4 for doc in await request.app.mongodb["tasks"].find().to_list(length=100): 5 tasks.append(doc) 6 return tasks
O roteador list_tasks é excessivamente simplista. Em um aplicativo do mundo real, você precisará, no mínimo, incluir a paginação. Felizmente, existem pacotes para a FastAPI que podem simplificar esse processo.
1 2 async def show_task(id: str, request: Request): 3 if (task := await request.app.mongodb["tasks"].find_one({"_id": id})) is not None: 4 return task 5 6 raise HTTPException(status_code=404, detail=f"Task {id} not found")
Embora a FastAPI seja compatível com Python 3.6+, é o meu uso de expressões de atribuição em roteadores como esse, e é por isso que esse aplicativo de exemplo exige Python 3.8+.
Aqui, estou criando uma exceção se não conseguirmos encontrar uma tarefa com o ID correto.
1 2 async def update_task(id: str, request: Request, task: UpdateTaskModel = Body(...)): 3 task = {k: v for k, v in task.dict().items() if v is not None} 4 5 if len(task) >= 1: 6 update_result = await request.app.mongodb["tasks"].update_one( 7 {"_id": id}, {"$set": task} 8 ) 9 10 if update_result.modified_count == 1: 11 if ( 12 updated_task := await request.app.mongodb["tasks"].find_one({"_id": id}) 13 ) is not None: 14 return updated_task 15 16 if ( 17 existing_task := await request.app.mongodb["tasks"].find_one({"_id": id}) 18 ) is not None: 19 return existing_task 20 21 raise HTTPException(status_code=404, detail=f"Task {id} not found")
Não queremos atualizar nenhum dos nossos campos para valores vazios, então, primeiro, removemos esses campos do documento de atualização. Como mencionado acima, como todos os valores são opcionais, uma solicitação de atualização com uma carga vazia ainda é válida. Depois de muita deliberação, decidi que, nessa situação, o certo a fazer pela API é retornar a tarefa não modificada e um status HTTP 200.
Se o usuário tiver fornecido um ou mais campos a serem atualizados, tentaremos
$set
os novos valores com update_one
, antes de retornar o documento modificado. No entanto, se não conseguirmos encontrar um documento com o ID especificado, nosso roteador emitirá um 404.1 2 async def delete_task(id: str, request: Request): 3 delete_result = await request.app.mongodb["tasks"].delete_one({"_id": id}) 4 5 if delete_result.deleted_count == 1: 6 return JSONResponse(status_code=status.HTTP_204_NO_CONTENT) 7 8 raise HTTPException(status_code=404, detail=f"Task {id} not found")
O roteador final não retorna um corpo de resposta em caso de sucesso, pois o documento solicitado não existe mais, já que acabamos de excluí-lo. Em vez disso, ele retorna um status HTTP 204, o que significa que a solicitação foi concluída com êxito, mas o servidor não tem dados para fornecer a você.
O front-end do React não muda, pois está apenas consumindo a API e, portanto, é um pouco agnóstico em relação ao back-end. São principalmente os arquivos padrão gerados por
create-react-app
. Portanto, para iniciar nosso front-end do React, abra uma nova janela de terminal - mantendo o servidor FastAPI em execução no terminal existente - e digite os seguintes comandos no diretório do front-end.1 npm install 2 npm start
Esses comandos podem demorar um pouco para serem concluídos, mas posteriormente, deve-se abrir uma nova janela do navegador para http://localhost:3000.
O front-end do React é apenas uma visualização da nossa lista de tarefas, mas você pode atualizar suas tarefas por meio da documentação da FastAPI e ver as alterações aparecerem no React!
A maior parte do código do front-end está em
frontend/src/App.js
1 useEffect(() => { 2 const fetchAllTasks = async () => { 3 const response = await fetch("/task/") 4 const fetchedTasks = await response.json() 5 setTasks(fetchedTasks) 6 } 7 8 const interval = setInterval(fetchAllTasks, 1000) 9 10 return () => { 11 clearInterval(interval) 12 } 13 }, [])
Quando nosso componente é montado, iniciamos um intervalo que é executado a cada segundo e obtemos a lista mais recente de tarefas antes de armazená-las em nosso estado. A função retornada no final do hook será executada sempre que o componente desmontar, limpando nosso intervalo.
1 useEffect(() => { 2 const timelineItems = tasks.reverse().map((task) => { 3 return task.completed ? ( 4 <Timeline.Item 5 dot={<CheckCircleOutlined />} 6 color="green" 7 style={{ textDecoration: "line-through", color: "green" }} 8 > 9 {task.name} <small>({task._id})</small> 10 </Timeline.Item> 11 ) : ( 12 <Timeline.Item 13 dot={<MinusCircleOutlined />} 14 color="blue" 15 style={{ textDecoration: "initial" }} 16 > 17 {task.name} <small>({task._id})</small> 18 </Timeline.Item> 19 ) 20 }) 21 22 setTimeline(timelineItems) 23 }, [tasks])
O segundo gancho é acionado sempre que a lista de tarefas em nosso estado muda. Este gancho cria um componente
Timeline Item
para cada tarefa em nossa lista.1 <> 2 <Row style={{ marginTop: 50 }}> 3 <Col span={14} offset={5}> 4 <Timeline mode="alternate">{timeline}</Timeline> 5 </Col> 6 </Row> 7 </>
A última parte de
App.js
é a marcação para renderizar as tarefas na página. Se você já trabalhou com MERN ou outra pilha React antes, isso provavelmente lhe parecerá muito familiar.Estou muito empolgado com a pilha da FARM, e espero que você também esteja. Podemos criar aplicativos web assíncronos e de alto desempenho usando minhas tecnologias favoritas! No meu próximo artigo, vamos ver como você pode adicionar autenticação aos seus aplicativos FARM.
Enquanto isso, confira a documentação da FastAPI e do Motor, bem como os outros pacotes e links úteis nesta lista Incrível da FastAPI.
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.