Back-end em Dart - Function Framework

Full Stack em Dart? 🤔

Faz algum tempo em que a Google vem trabalhando em um Framework que permite você escrever funções leves, como chamam que por sua vez executam pequenas ou, como desejar, tarefas, podendo hospedar as mesmas em Google Cloud Functions, Local Machine (desenvolvimento local), Knative - based environments, Google App Engine e Google Cloud Run, como descreve na documentação.

Nesse artigo vou abordar um maneira de criar serviço back-end em dart utilizando do Function Framework, vamos começar pelos requisitos.

1. Requisitos

  • Flutter
    • Caso não tenha o SDK do Flutter instalado basta acessar este link.
  • Dart
    • Variáveis de desenvolvimento Dart

      Devemos informar o path do SDK em que se encontra o Dart instalado para que o sistema reconheça comando do Dart quando utilizar o terminal.

      Caso tenha utilizado o método de instalação do Flutter mencionado no link acima poderá seguir os passos abaixo.

      Acessando o terminar navegar até a pasta

      cd Documentos/Library/flutter/bin/cache/dart-sdk/bin
      

      agora execute o comando

      pwd
      

      como na imagem.

      Copie a saída do comando e adicione no seu perfil do shell (no caso estou utilizando o zsh), irei editar o arquivo ~/.zshrc utilizando o gedit

      gedit ~/.zshrc
      

      ao final do arquivo adicionando a linha

      export PATH=$PATH:/home/<accountName>/Documentos/Library/flutter/bin/cache/dart-sdk/bin
      

      como na imagem.

      Feito isso poderá executar o comando dart no terminal, deverá ter um retorno como na imagem abaixo.

2. Instalação

Antes de mais nada devemos instalar o Dart Function Framework CLI ou dartfn utilizando o comando abaixo no terminal.

dart pub global activate dartfn

Ao finalizar a execução devemos ter um retorno como na imagem.

Como podemos observar é altamente recomendável adicionar o path $HOME/.pub-cache/bin utilizando a linha

export PATH="$PATH":"$HOME/.pub-cache/bin"

nas variáveis do sistema como foi feito com o Dart. Então faremos.

gedit ~/.zshrc

No final o arquivo ficará como na imagem.

E assim executando novamente dart pub global activate dartfn termos uma resposta positiva.

3. Criando o projeto (Hello World)

Como tudo em Dart e Flutter temos um templates pré programado para criação de projetos. Executando no terminal o comando

dartfn generate --list

teremos uma lista de exemplos prontos para adequarmos a nossa necessidade, iremos abordar o helloworld.

Então criaremos uma pasta onde executaremos o comando para que crie todo o projeto. Primeiro a pasta

mkdir Documentos/Workspace/dartfn_helloworld

após pasta criada navegaremos até ela

cd Documentos/Workspace/dartfn_helloworld

e assim criaremos o projeto

dartfn generate helloworld

como na imagem abaixo.

Visto que no terminal temos um retorno positivo sem falha alguma, ainda requer que executemos o comando pub get. Tal comando se faz necessário para que o Dart baixe todas as dependência do projeto.

Então ainda na pasta do projeto executemos o comando via terminal.

pub get

Neste momento temos o projeto criado e funcionando em perfeito estado, podemos testar o mesmo localmente.

Ainda na pasta do projeto execute

dart bin/server.dart

Pronto! Poderá acessar na porta 8080 a sua função executando.

Nesse ponto não temos mais limite! Podemos utilizar uma conexão com banco de dados e disponibilizar JSON como em qualquer outra api.

4. PostgreSQL

Agora iremos abrir o projeto no editor e configurar uma conexão com o banco de dados PostgreSQL para que disponibilizamos dados via JSON.

Primeiramente vamos preparar o ambiente PostgreSQL em Docker.

4.1. PostgreSQL (Docker)

Para instalar o Docker podemos simplesmente executar o comando abaixo.

sudo apt-get install docker-ce docker-ce-cli containerd.io

Feito isso, devemos carregar a imagem do PostgreSQL executando o comando.

sudo docker pull postgres

Apos ter carregado podemos criar uma instancia da imagem com o comando.

sudo docker run --rm --name pg-docker -e POSTGRES_PASSWORD=docker -d -p 5432:5432 -v /home/<accountName>/Documentos/Workspace/PostgreSQL:/var/lib/postgresql/data postgres

Com isso podemos executar o comando sudo docker ps para listar os processos do Docker em execução. No caso a saida dos comandos devem ser parecidas com a da imagem.

Pronto! Temos o PostgreSQL rodando em nosso sistema via Docker.

4.2. pgAdmin4

Caso queira, vamos instalar o pgAdmin utilizado para gerenciar nosso banco. Abaixo segue sequência de comandos para a instalação.

  1. Public Key
sudo curl https://www.pgadmin.org/static/packages_pgadmin_org.pub | sudo apt-key add
  1. Adicionando à source.list
sudo sh -c 'echo "deb https://ftp.postgresql.org/pub/pgadmin/pgadmin4/apt/$(lsb_release -cs) pgadmin4 main" > /etc/apt/sources.list.d/pgadmin4.list && apt update'
  1. Update
sudo apt-get update
  1. Instalando
sudo apt install pgadmin4-desktop

4.2.1. Configurando conexão com o banco

Agora iremos configurar a conexão do pgAdmin com o banco no Docker.

Execute o pgAdmin que foi instalado, a primeira vez irá abir uma tela como na imagem.

Para o Password informe docker então como na imagem crie inicie a criação de uma nova conexão.

Preencha o Name por exemplo com o nome “PostgreSQL Docker” e na guia Connection preencha como na imagem.

Feito isso deverá estabelecer uma conexão.

Agora com todo ambiente preparado partiremos para implementação!!!

5. Criando a tabela (TODO)

O exemplo que irei abordar vai ser o mais simplista possível, irei criar uma tabela para armazenar tarefas (Todo List) e disponibilizar a mesma no projeto criado anteriormente (hello world)

5.1. Criando a tabela

Com o pgAdmin aberto com o banco selecionado clique na ferramenta Query Tool como na imagem.

Irá abrir o editor SQL então execute o Script abaixo (poderá utilizar o atalho F5 para executar).

CREATE TABLE todo(
    id SERIAL
	, task VARCHAR(255)
	, createdAt TIMESTAMP
	, updatedAt TIMESTAMP
	, sync INTEGER
);

5.2. Inserindo dados

Com a tabela criada agora vamos inserir alguns registros com o Script abaixo. Selecione todos e utilizando F5 execute.

INSERT INTO todo (task, createdAt, updatedAt, sync) VALUES ('0001 TASK', NOW(), NOW(), -1);
INSERT INTO todo (task, createdAt, updatedAt, sync) VALUES ('0002 TASK', NOW(), NOW(), -1);
INSERT INTO todo (task, createdAt, updatedAt, sync) VALUES ('0003 TASK', NOW(), NOW(), -1);
INSERT INTO todo (task, createdAt, updatedAt, sync) VALUES ('0004 TASK', NOW(), NOW(), -1);
INSERT INTO todo (task, createdAt, updatedAt, sync) VALUES ('0005 TASK', NOW(), NOW(), -1);
INSERT INTO todo (task, createdAt, updatedAt, sync) VALUES ('0006 TASK', NOW(), NOW(), -1);
INSERT INTO todo (task, createdAt, updatedAt, sync) VALUES ('0007 TASK', NOW(), NOW(), -1);
INSERT INTO todo (task, createdAt, updatedAt, sync) VALUES ('0008 TASK', NOW(), NOW(), -1);
INSERT INTO todo (task, createdAt, updatedAt, sync) VALUES ('0009 TASK', NOW(), NOW(), -1);
INSERT INTO todo (task, createdAt, updatedAt, sync) VALUES ('0010 TASK', NOW(), NOW(), -1);

Conforme a imagem, foram adicionado 10 registros na tabela.

6. Programando a API

Irei utilizar o VsCode para programar a API em Dart. Abrindo o projeto que foi criado anteriormente “hello world” notamos uma grande semelhança sobre a estrutura de pastas de um projeto Flutter.

6.1. Adicionando dependência (pubspec.yaml)

Pois bem, o primeiro arquivo ser abordado será o pubspec.yaml responsável pelas dependências do projeto e suas configurações de versão.

No arquivo iremos adicionar uma bibliotéca responsável por realizar a conexão com o banco de dados PostgreSQL e alguns mais recursos interessantes. No caso o package posgres que atualmente se encontra na versão 2.3.2.

Em dependencies: do arquivo pubspec.yaml iremos adicionar a linha postgres: ^2.3.2, no meu caso o arquivo final ficou como no abaixo.

name: dartfn_helloworld
description: A sample "Hello, World!" Functions Framework project.
# version: 0.1.0
# homepage: https://www.example.com
publish_to: none

environment:
  sdk: ">=2.12.0 <3.0.0"

dependencies:
  functions_framework: ^0.4.0
  shelf: ^1.0.0
  postgres: ^2.3.2

dev_dependencies:
  build_runner: ^1.10.7
  functions_framework_builder: ^0.4.0
  http: ^0.13.0
  test: ^1.15.7
  test_process: ^2.0.0

6.2. Disponibilizando dados (functions.dart)

Após adicionar a dependência da biblioteca postgres no projeto daremos inicio a implementação sobre recuperar os dados do banco e disponibilizar via json em uma chamada rest.

Abrindo o arquivo functions.dart nos deparamos com uma única função com anotação @CloudFunction() essa seria a função principal, oque posso adiantar é que essa estrutura está preparada para trabalhar com micro serviços, mais a frente irei explicar melhor sobre isso, agora vamos nos atentar no básico.

Convertendo a Arrow Function para…

@CloudFunction()
Response function(Request request) {
  return Response.ok('Hello, World!');
}

Iremos instanciar uma nova conexão com o banco.

import 'package:functions_framework/functions_framework.dart';
import 'package:postgres/postgres.dart';
import 'package:shelf/shelf.dart';

@CloudFunction()
Response function(Request request) {
  var connection = PostgreSQLConnection(
    'localhost',
    5432,
    'postgres',
    username: 'postgres',
    password: 'docker',
  );
  
  return Response.ok('Hello, World!');
}

Apos feito isso iremos abrir a conexão.

import 'package:functions_framework/functions_framework.dart';
import 'package:postgres/postgres.dart';
import 'package:shelf/shelf.dart';

@CloudFunction()
Response function(Request request) {
  var connection = PostgreSQLConnection(
    'localhost',
    5432,
    'postgres',
    username: 'postgres',
    password: 'docker',
  );
  
  await connection.open();

  return Response.ok('Hello, World!');
}

Feito isso podemos recuperar os dados e armazenar em uma lista de map.

import 'package:functions_framework/functions_framework.dart';
import 'package:postgres/postgres.dart';
import 'package:shelf/shelf.dart';

@CloudFunction()
Response function(Request request) {
  var connection = PostgreSQLConnection(
    'localhost',
    5432,
    'postgres',
    username: 'postgres',
    password: 'doker',
  );
  
  await connection.open();

  List<Map<String, Map<String, dynamic>>> results = await connection.mappedResultsQuery(
    'SELECT id, task, sync FROM TODO;',
  );

  return Response.ok('Hello, World!');
}

Com os dados recuperados iremos agora converter os mesmos para json e retornar no response da chamada rest. Note que para ser feito isso o retorno devemos modificar a function para que retorne um Future especificando o tipo para < Response >.

import 'dart:convert';

import 'package:functions_framework/functions_framework.dart';
import 'package:postgres/postgres.dart';
import 'package:shelf/shelf.dart';

@CloudFunction()
Future<Response> function(Request request) async {
  var connection = PostgreSQLConnection(
    'localhost',
    5432,
    'postgres',
    username: 'postgres',
    password: 'docker',
  );
  
  await connection.open();

  List<Map<String, Map<String, dynamic>>> results = await connection.mappedResultsQuery(
    'SELECT id, task, sync FROM TODO;',
  );

  // print(json.encode(results));

  return Response.ok(json.encode(results));
}

Pronto!

6.3. Testando o projeto

Como podemos ver uma das dependências do projeto é o build_runner juntamente com o functions_framework_builder. Navegando para a pasta /bin do projeto podemos notar que existe um arquivo chamado server.dart, pois bem esse arquivo que é responsável pela execução do nosso servidor, o fato de termos o build_runner no projeto é para que esse arquivo server.dart seja gerado automáticamente a cada alteração nossa, o único que deve alterar o arquivo server.dart é somente o proprio build_runner conforme vamos alterando o arquivo functions.dart“há mas José até agora não vi alterações” 🧐, bem isso por que ainda não “startamos” o serviço do build_runner, então caso esteja utilizando o VsCode poderá seguir meus passos.

No menu Terminal selecione Run Task, então selecione a categoria dart, veja que irá aparecer várias opções para o build_runner então selecione dart: dart pub run build_runner build. Pronto! Feito isso irá abrir um terminal no próprio VsCode executando comandos de build, possívelmente irá aparecer uma mensagem para confirmação de conflitos como o build_runner gera código, ocasiona a necessidade de substituir arquivos já gerados no projeto (build’s old), nesse primeiro momento poderá optar pela opção 1 - Delete, então digitando 1 e teclando enter irá prosseguir.

Caso queria que o build_runner fique executando sempre e monitorando o arquivo modificado assim gerando sempre um novo arquivo server.dart a cada modificação do arquivo functions.dart basta em vez de escolher a opção dart: dart pub run build_runner build utilizar a dart: dart pub run build_runner watch.

O build_runner é um universo
Caso não conheça essa incrível ferramenta de geração de códigos para Dart recomendo uma leitura em sua documentação e em seu repositório para entender melhor do que se trata.

6.4. Executando o servidor (Dart)

Bom finalmente, com o ambiente preparado, com a implementação feita, podemos então executar o nosso back-end em Dart!!!

Ambiente preparado
Parece um processo trabalhoso até aqui, porém com toda essa parte feita, agora oque você precisa fazer é só estudar os bibliotecas e colocar a mão na massa que as ferramentas devidamente configuradas até aqui irão fazer seu trabalho, não necessitam mais interações 🥰

Já que estamos com o VsCode aberto no menu Terminal abriremos um novo terminal (new Terminal), feito isso irá abrir já na pasta do projeto. Basta executar o comando abaixo para que o servidor suba.

dart bin/server.dart

Como na imagem abaixo.

Assim acessando no browser ou utilizando alguma ferramenta de request no endereço localhost:8080 teremos…

Muito bom!! Dessa forma se torna simples a disponibilização de um serviço back-end para alguma aplicação, sem a necessidade de estar utilizando alguma outra linguagem, temos então Dart tanto no front-end quanto no back-end!! 😝🤯😎🚀🚀🚀

6.5. Executando o servidor (Debug)

À José eu quero debugar o servidor, e ae? 🤨

Pois bem, vamos preparar o VsCode para debug, como demostra na imagem abaixo na seção de debug do VsCode devemos criar o arquivo launch.json.

Devemos adicionar a entrada do programa no arquivo, no caso "program": "bin/server.dart", deverá ficar como o arquivo abaixo.

{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "name": "dartfn_helloworld",
            "program": "bin/server.dart",
            "request": "launch",
            "type": "dart"
        }
    ]
}

Pronto, agora basta acessar o menu Run e clicar em Start Debugging.

Vamos testar um request com o breakpoint marcado no return da request. Irei demostrar o caso utilizando o insomnia.

Instalando o Insomnia
# Add to sources
echo "deb https://dl.bintray.com/getinsomnia/Insomnia /" \
    | sudo tee -a /etc/apt/sources.list.d/insomnia.list
# Add public key used to verify code signature
wget --quiet -O - https://insomnia.rest/keys/debian-public.key.asc \
    | sudo apt-key add -
# Refresh repository sources
sudo apt-get update
# Install Insomnia
sudo apt-get install insomnia

Como podemos ver na imagem a requisição no Insomnia está aguardando o retorno, e no VsCode na linha 25 está parada a execução no breakpoint.

Após liberar o breakpoint…

6.6. Executando o servidor (Deploy 🧐)

À legal José eu quero fazer deploy do trêm ae, tem que ter o Dart no servidor??? 🤨😖

Então meu amigo, NÃO!, Ué como assim?? 😐!!

Precisa não, com o dart compile podemos converter o nosso código dart em binário!! Vou demostrar…

Com o terminal ou diretamente no VsCode, podemos acessar o arquivo server.dart criado pelo build_runner e executar a compilação do mesmo, como no exemplo abaixo.

dart compile exe bin/server.dart

Então acessando o diretório bin/ do projeto teremos o server.exe… isso mesmo, um servidor compilado para execução nativa, não precisa de nenhuma conversão de linguagem, nem mesmo instalar o próprio Dart, vm ou algo do tipo no seu servidor de aplicação para que possa executar… 😶😶😶

Vamos fazer mais um teste, vamos executar esse binário pelo terminal e ver se o resultado sobre a requisição programada continua funcionando…

6.7. Microsserviço

Como mencionado mais acima o Function Framework tem a possibilidade de trabalhar com abordagem de Microsserviços(microservices) além da demostrada(Monolítica).

Arquitetura Monolítica x Microsserviços

Oque difere a arquitetura de Microsserviços para Monolítica tradicionais é a decomposição da aplicação segmentada por funções básicas e de responsábilidade única (bom pelo menos seria a ideia, na pratica fica um pouco mais complicado), de maneira que cada função é denominada serviço e pode ser criada e implementada de maneira independente. Isso significa que cada serviço(função) funciona sem a dependência de outro e por consequência poder falhar sem comprometer os demais. Irei deixar uma imagem explicando essa técnica.

Vamos transformar o nosso projeto para que trabalha na arquitetura de Microsserviços. Com o VsCode aberto no arquivo functions.dart e a task do build_runner em execução iremos adicionar a nossa função hello world novamente logo abaixo da função já criada. Como no exemplo abaixo.

import 'dart:convert';

import 'package:functions_framework/functions_framework.dart';
import 'package:postgres/postgres.dart';
import 'package:shelf/shelf.dart';

@CloudFunction()
Future<Response> function(Request request) async {
  var connection = PostgreSQLConnection(
    'localhost',
    5432,
    'postgres',
    username: 'postgres',
    password: 'docker',
  );
  
  await connection.open();

  List<Map<String, Map<String, dynamic>>> results = await connection.mappedResultsQuery(
    'SELECT id, task, sync FROM TODO;',
  );

  // print(json.encode(results));

  return Response.ok(json.encode(results));
}

Response helloWorld(Request request) {
  return Response.ok('Hello World');
}

Antes de mais nada quero que note algo no arquivo server.dart, veja que no mesmo existe a implementação de um switch e na clausula case a operação 'function', sendo assim caso o nome da função “target” (alvo) seja 'function' ele irá executar o bloco function_library.function. Pois bem, e o nosso hello world? Ae que está o pulo do gato, devemos colocar a anotação @CloudFunction() em cima da função criada, para que o build_runner reconheça e gere o código devido. Bem vamos lá então…

@CloudFunction()
Response helloWorld(Request request) {
  return Response.ok('Hello World');
}

Agora com a anotação no seu devido lugar e a task do build_runner executando corretamente, deverá ter gerado o nosso código no arquivo server.dart. Segue abaixo como ficou.

import 'package:functions_framework/serve.dart';
import 'package:dartfn_helloworld/functions.dart' as function_library;

Future<void> main(List<String> args) async {
  await serve(args, _nameToFunctionTarget);
}

FunctionTarget? _nameToFunctionTarget(String name) {
  switch (name) {
    case 'function':
      return FunctionTarget.http(
        function_library.function,
      );
    case 'helloWorld':
      return FunctionTarget.http(
        function_library.helloWorld,
      );
    default:
      return null;
  }
}

Muito bom!! Mas e ae José, como faço pra executar essas duas funções? 😑.

Bem, antes disso vamos gerar um novo binário, mas nada impede de executar como já demostrei anteriormente! Executando comando abaixo como já explicado iremos compilar o arquivo.

dart compile exe bin/server.dart

Com o binário compilado por questão didática irei executar o arquivo via terminal, na verdade com dois terminais. Para que você consiga executar uma função específica implementada precisará apontar para um “alvo”, como demostro no comando abaixo.

./bin/server.exe --target helloWorld

Dessa forma o serviço irá subir apontado para uma específica função.

Pois bem, agora podemos prepara o ambiente com dois terminais e executar as duas funções ao mesmo tempo assim simulando dois serviços independente, a função helloWorld que nos retorna uma simples String e a funcion implementada anteriormente, que por sua vez nos retorna um json com os dados da tabela todo.

No primeiro terminal prepararemos o comando abaixo, nada de novo tudo normal pois já estávamos utilizando dessa forma anteriormente.

./server.exe

Logo no segundo terminal, prepararemos o comando baixo. Nesse caso iremos passar a porta 8081 pois o primeiro terminal já ira ocupar por default a porta 8080 e não queremos que ocasione conflito.

Ficará como na imagem.

./server.exe --target helloWorld --port 8081

Então executaremos os dois comandos e prepararei o insomnia para os testes.

Dessa forma com um mesmo servidor e diversos “alvos” vocẽ poderá segmentar a sua aplicação conforme achar necessário…

Acredito que tenha chegado ao fim de mais um artigo, sei que ficou uma leitura bem extensa, mas pode ser bem proveitoso ter chegado até aqui, quero agradecer você leitor por estar aqui, espero ter ajudado pelo menos no minimo a ter uma noção do conteúdo!!

Grande abraço e até a próxima 😄

0%