API Simples com FastAPI e NodeJS com Express e dando deploy com Deta Space CLI
Depois de um bom tempo resolvi atualizar um post que fiz a muito tempo, já que o Deta mudou! e tornou-se Deta Space, um novo serviço que pode fazer o que o antigo deta fazia e muito mais!, Nesse post abordarei o desenvolvimento de uma API em Python com um FrontEnd em NodeJS e subir a aplicação no serviço de apps do Deta Space. |
Antes de começar, vamos entender o que é uma API REST.
Uma API REST basicamente é um conjunto de restrições definidas para o comportamento da interface de comunicação de equisições http para aplicações que trabalham com dados com o servidor. Desta forma, uma api rest permite um comportamento “padrão” restrito para uma aplicação executar determinadas operações com a API.
Uma API REST em suma, é representada pelas quatro operações básicas no tratamento de dados: CREATE, READ, UPDATE, DELETE. (CRUD). No caso das REST API’s, o comportamento é definido na requisição do cabeçalho http, utilizado os seguintes principais: GET, POST, PUT, DELETE.
Instalando os componentes e estruturando o projeto
FastAPI é um micro framework para python, usado para criar api’s REST simples, com ele é possível criar uma API RESTful com poucas linhas de código. Pode-se dizer que seu uso tão simples como o Framework Python Web Flask. Para dar seguimento, vamos estruturar nosso projeto da seguinte forma para facilitar o trabalho futuro, crie uma pasta para o seu projeto e crie a seguinte estrutura de diretórios.
|-backend <---Vamos focar no Backend por enquanto
| |-- app
| | |---- database
| | | |---- __init__.py
| | | |---- db_con.py
| | |---- models
| | | |---- __init__.py
| | | |---- models.py
| | |---- routers
| | | |---- __init__.py
| | | |---- api.py
| | |----__init__.py
| | |---- config.py
| |---- .env
| |---- main.py
| |---- requirements.txt
|-frontend
| |---- index.js
|.......
Nota*: O FastAPI não possui uma ‘arquitetura de projeto’ definida, isso dependerá da aplicação. Para aplicações que são grandes, o mais indicado seria utilizar uma alternativa mais robusta,
Antes também devemos instalar as seguintes dependencias (deta, fastapi e uvicorn e a python-dotenv), é possivel fazer isso simplismente criando um arquivo requirements.txt e colar a informação abaixo e rodar o comando pip install -r requirements.txt
, lembre-se sempre de salvar o seu requirements.txt em formato UTF-8, caso você tenha criado ele manualmente.
deta
fastapi
python-dotenv
uvicorn
Para começar a utilizar primeiro é importante ter o Python instalado e depois é só instalar o módulo com o comando pip install fastapi
e criar o seu arquivo main.py.
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"Hello": "World"}
Em seguida, rode o seguinte comando uvicorn main:app --host 0.0.0.0 --port 80 --reload
no terminal que o seu primeiro projeto em FastAPI irá ser iniciado. Basta digitar no navegador ou no seu programa do Postmam ou Insomnia e fazer a requisição do tipo GET na rota http://0.0.0.0:80/ ou simplismente http://localhost:80/.
Criando os modelos da API
from typing import Optional, List
from pydantic import BaseModel, Field
from datetime import datetime
class Leitura(BaseModel):
sensor: Optional[str] = Field(None, example="Sensor Name")
desc: Optional[str] = None
status: Optional[List['str']] = Field([], example=[ "Leitura 1", "Leitura 2", "..."])
data_hora: int = int(datetime.timestamp(datetime.now()) * 1000)
class Config: # somente para documentação
json_schema_extra = {
"example": {
"sensor": "DHT11",
"desc": "Monitorando...",
"status": [
"Temperatura: 30.0 ºC",
"Umidade: 60%"
]
}
}
Crie o modelo acima em seu arquivo models.py, o modelo apresenta atributos que irão compor o nosso Payload Json na requisição, além disso temos diferentes atribuições para cada um dos atributos, por exemplo:
- O atributo sensor, é opcional de ser passado, sendo do tipo string, tendo campo padrão como None (tipo null em python);
- O atributo desc, é opcional de ser passado, sendo do tipo string, iniciando com None;
- O atributo status é opcional, sendo uma lista de strings, além disso começa sempre como uma string vazia;
- O atributo data_hora, é do tipo inteiro, e sempre é iniciado quando o objeto é criado;
- A classe interna Config, serve apenas para documentação dos nossos models na API, assim como os atributos 'examples';
Deta Base e Deta Space (Atualizado)
Agora que temos os nossos modelos, antes de dar prosseguimento na aplicação, devemos criar uma conta Deta Space para poder ter acesso a criação de projetos e usar o Deta Base, e também instalar o Space CLI pra fazer deploy da nossa API.
O Novo ambiente do Deta possui um ambiente de criação (agora chamado de Canvas), onde é possivel acessar a documentação, criar colleções de dados (Parecido com os bancos NoSQL) e mais. Mas ainda mantém algumas das caracteristicas do antigo Deta, como o Deta Base, Drive e o ambiente para deploy dos microserviços. Caso queira saber mais acesse a documentação oficial do Deta Space apra criação de novos apps. Space Docs,
Agora com o Deta Base é possível salvar dados na nuvem no formato de documentos NoSQL de forma simples e fácil, apesar das limitações no numeros de requisições e do tempo entre elas (Veja sempre a documentação).
Após criar a conta no Deta, crie um App no "Builder", é possivel criar um do zero ou usar um projeto já existente, o comando para criar um app é usar o comando space new
. Isso ira gerar um arquivo chamado “Spacefile”, caso você esteja acostumado com o Docker vai acabar se familizarizando com a forma de definir configurações nele. Vamos edita-lo para deixar assim:
# Spacefile Docs: https://go.deta.dev/docs/spacefile/v0
v: 0
micros:
- name: backend
src: ./backend
engine: python3.9
path: api
dev: uvicorn main:app --reload
Para rodar a aplicação no Windows, é necessário ativar a sua venv do python caso esteja usando python. O comando “space new” automaticamente reconhece o tipo de projeto que você está usando, mas é possivel edita-lo, no nosso micro vamos por enquanto mecher no backend, então deixaremos assim, agora vamos explicar algumas coisas:
- name: é o nome do nosso microserviço;
- src: é o diretório onde ele se encontra, caso esteja na raiz, é só usar “.”;
- engine: é a engine do nosso projeto, o Deta Space possui suporte a vários tipos para micro serviços, veja a documentação
- path: não será util agora, mas ele serve para quando nós criarmos o frontend podermos acessar ele por meio do http://localhost:4200/api/ como estamos usando apenas o backend, ele não é util agora, (A porta 4200 é o padrão do Space Dev, Altera-la pode dar ruim acredite), mas não se preocupe que em produção você pode deixar outra porta no seu app.
- dev: o comando ao rodarmos no terminal
space dev
, desta forma ele irá emular um ambiente do Deta Space.*
*Antigamente, para usar o Deta Base e o Deta Drive, era necessário definir as variaveis de ambiente com as Keys Geradas, atualmente o Deta já faz isso para nós caso desejemos usar o Base e Drive no mesmo App. Para isso, devemos usar o comando space dev para termos acesso a essas variáveis, já que o Deta atual não reconhece o arquivo “.env” que sempre usamos para definir nossas variaveis de teste. Caso você queira usar uma key de um projeto em outro você também pode gera-la e passar ela como parametro ao usar o Deta, basta acessar o seu app que deve estar criado e acessar a parte de desenvolvimento, lembre-se que a Key de um projeto só pode ser exibida uma unica vez, então salve ela em um local seguro!
O Deta possui algumas variaveis de ambiente já predefinidas.
Essas variaveis são acessadas quando você usa o space dev
ou quando você dá deploy do seu app com o space push
. Mas também é possivel alterar ou passar novas variaveis de ambiente ou sobrescrever-las, basta no nosso Spacefile definir elas como presets, todas as variaveis devem estar no formato String, ex:
# Spacefile Docs: https://go.deta.dev/docs/spacefile/v0
v: 0
micros:
- name: backend
src: ./backend
engine: python3.9
path: api
dev: uvicorn main:app --reload
presets: # <--- Define os presets
env:
- name: VAR_APP_NAME
description: "Descrição da variavel de ambiente"
default: "true"
Tambem é possiel alterar os valores padrão das variaveis no proprio Build apois o deploy do app, basta acessar a parte de desenvolvimento e configuração:
Caso você não queira usar o padrão do deta ou usar um App já existente com as Keys definidas para seus testes, e queira usar um ambiente proprio, é só usar um arquivo .env, e setar as variaveis para usar essas variáveis localmente no nosso projeto, sem precisar defini-las no sistema, podemos usar o python dotenv que instalamos, no nosso arquivo config.py adicionaremos o seguinte.
import os
from dotenv import load_dotenv # Usar só localmente, devendo ser desconciderado ao dar deploy
load_dotenv() # Lê o arquivo .env
DETA_BASE_NAME = os.getenv("VAR_DETA_BASE_NAME")
DETA_PROJECT_KEY = os.getenv("VAR_DETA_PROJECT_KEY")
DETA_PROJECT_ID = os.getenv("VAR_DETA_PROJECT_ID")
Desta forma é possivel usar as variáveis criadas no nosso arquivo .env localmente, com isso feito vamos dar prosseguimento ao desenvolvimento da nossa API para em seguida usar o Deta CLI, no nosso arquivo api.py vamos adicionar uma forma de criar nossas rotas e dizer que queremos usar o nosso model 'Leitura' como Payload. Vamos criar quatro funções que irão representar nosso CRUD.
from ..models.models import Leitura
from .config import DETA_BASE_NAME
# from .config import DETA_PROJECT_KEY, DETA_PROJECT_ID
from typing import List
from fastapi.routing import APIRouter
# Instancia da nossa base do Deta pra salvar nossos dados, definindo os parametros pelo nosso arquivo config.py
db = Deta().Base("leituras")
# ou
# caso você queira passar uma key específica para o seu projeto como na forma antiga
# db = Deta(
# project_key=DETA_PROJECT_KEY,
# project_id=DETA_PROJECT_ID
# ).Base(DETA_PROJECT_BASE)
# rota para a nossa API do FastAPI
router = APIRouter()
@router.get('/', status_code=200)
def get_all():
"""
Busca e retorna todos os objetos do deta base
:returns: List
"""
return db.fetch().items
@router.get('/{key}', status_code=200)
def get_one(key: str):
"""
Busca e retorna um item pela key do deta base
:returns: json
"""
return db.get(key)
@router.post('/', status_code=201)
def post_one(leitura: Leitura):
"""
cria um item no deta base
:returns: json
"""
return db.insert(leitura.model_dump())
@router.get('/{key}', status_code=201)
def put_one(key: str, leitura: Leitura):
"""
Busca e atualiza um documento no deta base pela key
:returns: json
"""
_finded = db.get(key)
to_update = Leitura(**(_finded if _finded != None else {}))
new_data = leitura.model_dump(exclude_unset=True)
updated = to_update.model_copy(update=new_data)
return db.put(updated.model_dump(), key)
@router.get('/{key}', status_code=204)
def delete_one(key: str):
"""
Deleta um documento do deta base
"""
db.delete(key)
No nosso main.py adicionaremos algumas coisas, primeiro vamos criar uma função para criar o objeto do nosso app, além disso, devemos usar as seguintes configurações do CORS, de modo a permitir que as rotas da aplicação possam ser acessadas por qualquer endereço caso dermos deploy nela mais tarde. Em seguida adicionamos a rota principal da nossa api e colocamos o prefixo ‘/leituras’, assim para acessar a rota base ‘/’ da api com o GET request, ao rodarmos o comando uvicorn main:app --host 0.0.0.0 --port 80 --reload
, vamos acessar da seguinte forma, http://0.0.0.0:80/leituras ou simplismente http://localhost:80/leituras.
Entretanto, como vamos usar o space dev
, nós vamos acessar a nossa api com http://localhost:4200/leituras/
from .routers.api import router as api_router
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
def create_app():
_app = FastAPI()
_app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
_app.include_router(api_router, prefix='/leituras')
return _app
app = create_app()
Agora finalmente vamos usar o comando space dev
para acessar nosso entpoint, vamos inserir um valor qualquer, vou estar usando o software Insomnia:
Após usar o methodo POST, vamos ver os dados na nossa aplicação no canvas, para isso acesse o builder>suaaplicacao>develop>data para visualizar os dados, se nada aparece basta clicar na opção de select data e marcar o "leituras" que foi o que definimos.
Pronto usa aplicação está rodando com sucesso, agora que as coisas estão funcionando, vamos criar um frontend para visualizar os dados. Nós vamos fazer em nodejs, então navegue e inicie um projeto Node na pasta frontend com o comando node init
, e em seguida use o seguinte comando npm i express cors axios
.
Criando o Frontend
Primeiro vamos importar o necessário:
const express = require("express");
const cors = require("cors");
const app = express();
/**
* O Deta Space usa o Node v16, então ele não possui suporte ao fetch dentro do proprio node
*/
const axios = require('axios');
app.use(cors()); // para atender as requisições na mesma rota e evitar problemas
Em seguida vem a parte "complexa", vai ficar um pouco grande mas não é nada complicado, vamos colocar um pouco de html para criar uma tabela e exibir os dados que formos inserindo via API.
app.get("/", (req, res) => {
axios.get(
`${process.env.VAR_APP_PRODUCTION != "false" ? `https://${req.hostname}` : 'http://127.0.0.1:4200'}/api/leituras/`
)
.then(response => {
const json = response.data;
var html =
`
<html>
<head><style>
body { display: flex; justify-content: center; }
table, th, td { border: 1px solid; }
th, td { padding: 9px; }
</style></head>
<body>
<table>
<tr>
<th>Sensor</th>
<th>Descrição</th>
<th>Status</th>
<th>Data</th>
</tr>
${json.map((e, i) => {
return `<tr>
<td>${e.sensor}</td>
<td>${e.desc}</td>
<td>${e.status.toString()}</td>
<td>${new Date(e.data_hora).toUTCString()}</td>
</tr>`;
})}
</table>
</body>
</html>
`;
res.send(html);
}).catch(err => {
res.send(`<p>${err.message}</p>`);
})
});
Por fim colocamos o nosso frontend para atender as requisições, lembre-se que o Deta Dev usa a porta 4200, então o app principal praticamente fica na porta 4201, assim podemos rolar localmente pelo comando space dev.
const VAR_APP_PRODUCTION = process.env.VAR_APP_PRODUCTION;
const VAR_APP_PORT = process.env.PORT || 4201;
if(VAR_APP_PRODUCTION){
app.listen(VAR_APP_PORT, () => console.log(`Server as started at port ${VAR_APP_PORT}`));
}
Agora editamos o nosso arquivo Spacefile
# Spacefile Docs: https://go.deta.dev/docs/spacefile/v0
v: 0
micros:
- name: frontend
src: ./frontend
engine: nodejs16
primary: true
dev: node index.js
run: "node index.js"
public_routes: # <--- Para deixar a nossas rotas publicas e acessa-las externamente
- "/*"
presets:
env:
- name: VAR_APP_PRODUCTION
description: "If this app is in production"
default: "true"
- name: backend
src: ./backend
engine: python3.9
path: api
dev: uvicorn main:app --reload
No final, usaremos o comando space push
para dar deploy do nosso App no Builder do Space, é possivel ver que ele vai instalar as dependencias do nosso app, por isso é necessário o requirements.txt do python, pois sem ele nossa api não funciona. Após o deploy ele vai exibir no terminal a rota do nosso app que pode ser acessada publicamente, já que usamos o "public" no nosso Spacefile. Sem isso teriamos que estar logados o tempo todo no Deta para acessar a roda da API, o que não seria util nesse caso em específico.