v2.0

Postado em: 07-08-2023 às 11:20:48 PM · 13 min de leitura

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. 

Tags:
Posts relacionados
Redes Neurais e algoritmos genéticos - Joguinhos simples
Portainer e NGINX
ESP8266 Projeto - Consultando cotações em APIs de criptomoedas
NodeMCU ESP8266 - As placas para IoT