DEV Community

Cover image for Google Places API: criando um App em Go para testar o novo recurso de Text Search
Marcelo Matz
Marcelo Matz

Posted on

Google Places API: criando um App em Go para testar o novo recurso de Text Search

Eu recentemente coloquei um desafio no meio das minhas metas: implementar uma aplicação fullstack usando a linguagem de programação Go. E eu consegui 🤘

Este artigo é sobre a minha experiência implementando um novo recurso da API de Places do Google Maps usando Go no backend e JS no frontend.

Se você quer uma versão resumida e em vídeo deste artigo, aqui você encontra.

Depois da PoC do BuscaCep usando DDD e CleanCode onde eu usei a API do ViaCep, eu comecei a pesquisar mais sobre o assunto e obviamente eu acabei esbarrando na plataforma do Google Maps.

Neste artigo eu mostro passo a passo o que eu fiz para implementar o App, qual foi a linha de raciocínio que eu usei para criar cada coisa e ainda mostro o código que eu usei, tanto no backend quanto no frontend.


Google Maps

O Google Maps é a plataforma do Google que te permite criar apps usando todas as informações que o Google tem sobre endereços.

Só o aprendizado para entender como o Google Maps é segmentado e principalmente como é feita a cobrança do serviço, já foi um baita aprendizado.

A plataforma do Google Maps é dividida em três segmentações diferentes:

  • Maps
  • Routes
  • Places Image Segmentação das APIs do Google Maps

Maps é a API responsável por:

  • Mapas estáticos: Mostre um mapa estático no seu site.
  • Imagens do Street View: Adicione imagens do Street View em 360° aos seus apps.
  • Mapas de Elevação de terreno: Conseguir os dados da elevação de um ou vários locais.

Routes é a API responsável por:

  • API Routes: Versão otimizada para a performance das APIs Directions e Distance Matrix com recursos extras.
  • Roads: Identifique vias próximas usando as coordenadas.
  • Rotas: Forneça rotas para vários meios de transporte, com informações do trânsito em tempo real.
  • Matriz de distância: Calcule os tempos de viagem e as distâncias para várias origens e destinos.

Places é a API responsável por:

  • API Places: Integre o Place Details, Search e Autocomplete do Google aos seus apps.
  • Geocodificação: Converta coordenadas em endereços e vice-versa.
  • Geolocalização: Veja a localização aproximada do dispositivo usando as torres de celular e os nós da rede Wi-Fi próximos.
  • Address Validation: _Valide um endereço e seus componentes.
  • Fusos horários: Descubra o fuso horário de um conjunto de coordenadas._

API Places

Dentro dessa infraestrutura de APIs do Maps, o meu projeto se limitou a implementar a API Places que é justamente a API que integrar o Place Details, o Search e também o Autocomplete que eu não usei neste projeto.

A aplicação foi dividida em 2 partes: backend e frontend.

O objetivo principal era implementar a parte de backend da API de Places usando o Search e o Place Details usando a linguagem Go, sem deixar de criar um frontzinho básico.

Parte 1: Backend
A aplicação backend foi escrita em Go, e o papel desse backend era prover uma API que fosse responsável por buscar informações do Google Places API para depois retornar elas para o frontend. Ao meu ver esse este é básico de uma aplicação web, hoje em dia.

Alguns recursos do backend em Go:

  • Buscar lugares através da Google Places API
  • Manejar CORS (isso consumiu algumas horas de estudo)
  • Servir as informações obtidas para o frontend

Como a API do Google Place exige que eu faça duas requisições, uma para buscar o local e outra para buscar os detalhes deste local, eu fiz uma implementação da seguinte forma:

  1. Função SearchPlace
func searchPlace(client *http.Client, query string) (*PlaceSearch, error) {
    searchURL := fmt.Sprintf("https://maps.googleapis.com/maps/api/place/textsearch/json?query=%s&key=%s", url.QueryEscape(query), gcpToken)
    resp, err := client.Get(searchURL)
    if err != nil {
        return nil, err
    }
    defer func(Body io.ReadCloser) {
        err := Body.Close()
        if err != nil {
            log.Println(err)
        }
    }(resp.Body)

    respBody, err := io.ReadAll(resp.Body)
    if err != nil {
        return nil, err
    }

    placeSearch := PlaceSearch{}
    if err := json.Unmarshal(respBody, &placeSearch); err != nil {
        return nil, err
    }

    return &placeSearch, nil
}
Enter fullscreen mode Exit fullscreen mode

Ela tem a função de realizar uma consulta de pesquisa na API do Google Places, utilizando o termo de pesquisa fornecido como argumento query.

A chamada HTTP para a API é feita e a resposta (um JSON) é desserializada num objeto PlaceSearch, que contém um array de resultados, cada um com um PlaceId correspondente.

Se ocorrer um erro durante a execução da chamada HTTP ou durante a desserialização do JSON, o erro será retornado. Caso contrário, a função retorna o objeto PlaceSearch.

  1. Função getPlaceDetail
func getPlaceDetail(client *http.Client, placeId string) (*PlaceDetail, error) {
    detailURL := fmt.Sprintf("https://maps.googleapis.com/maps/api/place/details/json?place_id=%s&fields=name,formatted_address,formatted_phone_number,website,rating,business_status,opening_hours,reviews,url,price_level&key=%s&language=pt-BR", placeId, gcpToken)
    resp, err := client.Get(detailURL)
    if err != nil {
        return nil, err
    }
    defer func(Body io.ReadCloser) {
        err := Body.Close()
        if err != nil {
            log.Println(err)
        }
    }(resp.Body)

    body, err := io.ReadAll(resp.Body)
    if err != nil {
        return nil, err
    }

    placeDetail := PlaceDetail{}
    if err := json.Unmarshal(body, &placeDetail); err != nil {
        return nil, err
    }

    return &placeDetail, nil
}
Enter fullscreen mode Exit fullscreen mode

Essa função é utilizada para obter detalhes de um local específico, identificado pelo seu PlaceId.

A função faz uma chamada HTTP para um endpoint diferente da API do Google Places, que retorna os detalhes do local.

A resposta (um JSON) é desserializada num objeto PlaceDetail, que contém várias informações, incluindo nome, endereço formatado, URL do website, horário de funcionamento, reviews e mais.

Da mesma forma que searchPlace, se ocorrer um erro durante a chamada HTTP ou na desxerialização do JSON, o erro será retornado. Caso contrário, a função retorna o objeto PlaceDetail.

As duas funções desempenham um papel crucial na interação com a API do Google Places - a searchPlace é utilizada para fazer consultas de busca gerais, enquanto a getPlaceDetail é utilizada para obter detalhes mais específicos sobre cada local retornado pela pesquisa.

  1. Função searchHandler Essa função é o coração deste software.
func searchHandler(w http.ResponseWriter, r *http.Request) {
    client := &http.Client{Timeout: 10 * time.Second}

    searchStr := r.URL.Query().Get("query")

    placeSearch, err := searchPlace(client, searchStr)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    totalPlaces := len(placeSearch.Results)

    details := make([]*PlaceDetail, totalPlaces)
    var wg sync.WaitGroup

    for i, result := range placeSearch.Results {
        wg.Add(1)
        go func(i int, placeId string) {
            defer wg.Done()
            detail, err := getPlaceDetail(client, placeId)
            if err != nil {
                http.Error(w, err.Error(), http.StatusInternalServerError)
                return
            }
            details[i] = detail
        }(i, result.PlaceId)
    }

    wg.Wait()

    w.Header().Set("Content-Type", "application/json")
    err = json.NewEncoder(w).Encode(details)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
}
Enter fullscreen mode Exit fullscreen mode

A função searchHandler é um manipulador HTTP que é acionado quando o endpoint /search é acessado.

No começo da função, um cliente HTTP com um limite de tempo de 10 segundos é criado. Em seguida, ela extrai a string de pesquisa a partir da query do request HTTP.

O método searchPlace é então chamado com o cliente HTTP e a string de pesquisa, que faz uma requisição para a API Place do Google e retorna uma matriz de PlaceIds.

Depois, a função cria um slice para armazenar as respostas de PlaceDetail vindas da API do Google Places.

A função inicia uma gorotina para cada PlaceID obtido na resposta da searchPlace, e estas gorotinas fazem chamadas à função getPlaceDetail para obter detalhes para cada PlaceID.

O sincronismo dessas gorotinas é controlado utilizando um WaitGroup.

Ao terminar as chamadas à função getPlaceDetail, a função searchHandler configura o cabeçalho "Content-Type" do response como "application/json" e codifica o slice de PlaceDetails em JSON para enviar como resposta ao cliente.

Se algum erro ocorrer ao longo desses passos, a função irá retornar um status de erro HTTP 500 para o cliente junto com o erro que ocorreu.

// Esta função é chamada quando o endpoint /search é acessado.
func searchHandler(w http.ResponseWriter, r *http.Request) {
    // Criar um cliente HTTP com um tempo limite de 10 segundos.
    client := &http.Client{Timeout: 10 * time.Second}

    // Extrair a string de pesquisa a partir da query do request HTTP.
    searchStr := r.URL.Query().Get("query")

    // Faz uma chamada à API do Google Places para procurar por um lugar.
    placeSearch, err := searchPlace(client, searchStr)
    if err != nil {
        // Se houver um erro, retorna um status 500 e o erro.
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    // Cria um slice para armazenar os detalhes dos lugares.
    totalPlaces := len(placeSearch.Results)
    details := make([]*PlaceDetail, totalPlaces)

    // Inicializando um WaitGroup para sincronizar as goroutines
    var wg sync.WaitGroup

    // Faz uma chamada para obter detalhes de cada lugar baseado em PlaceID
    for i, result := range placeSearch.Results {
        wg.Add(1)
        go func(i int, placeId string) {
            // Chamando a função para obter os detalhes do local
            defer wg.Done()
            detail, err := getPlaceDetail(client, placeId)
            if err != nil {
                http.Error(w, err.Error(), http.StatusInternalServerError)
                return
            }
            details[i] = detail
        }(i, result.PlaceId)
    }

    // Esperando todas as goroutines terminarem
    wg.Wait()

    // Configurar o cabeçalho HTTP 'Content-Type' para 'application/json'
    w.Header().Set("Content-Type", "application/json")

    // Codificar a resposta em JSON
    err = json.NewEncoder(w).Encode(details)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
}
Enter fullscreen mode Exit fullscreen mode

Structs

Para sustentar tudo isso e também para eu conseguir tratar o JSON recebido, eu criei quatro structs.

type PlaceSearch struct {
    Results []struct {
        PlaceId string `json:"place_id"`
    } `json:"results"`
}

type OpeningHours struct {
    OpenNow     bool     `json:"open_now"`
    WeekdayText []string `json:"weekday_text"`
}

type Review struct {
    AuthorName string `json:"author_name"`
    Text       string `json:"text"`
}

type PlaceDetail struct {
    Result struct {
        Name                 string        `json:"name"`
        FormattedAddress     string        `json:"formatted_address"`
        FormattedPhoneNumber string        `json:"formatted_phone_number"`
        Website              string        `json:"website"`
        Rating               float64       `json:"rating"`
        BusinessStatus       string        `json:"business_status"`
        OpeningHours         *OpeningHours `json:"opening_hours"`
        Reviews              []Review      `json:"reviews"`
        Url                  string        `json:"url"`
        PriceLevel           int           `json:"price_level"`
    } `json:"result"`
}
Enter fullscreen mode Exit fullscreen mode

Main

A minha função main, responsável por iniciar o meu software, acabou ficando assim:

func main() {
    err := os.Getenv("GCP_TOKEN_PLACE_API")
    if err != nil {
        log.Fatalf("Erro ao carregar o arquivo .env")
    }

    if gcpToken == "" {
        log.Fatal("Insira o Token GCP_TOKEN_PLACE_API na variável de ambiente")
    }

    http.HandleFunc("/search", searchHandler)

    log.Fatal(http.ListenAndServe(":8080", handlers.CORS(handlers.AllowedOrigins([]string{"*"}))(http.DefaultServeMux)))
}
Enter fullscreen mode Exit fullscreen mode

Essa função basicamente verifica se o token de autenticação está inserido na variável de ambiente. (Em alguns momentos eu usei a variável de ambiente setada no meu OS, em outros momentos usei .env e em outros o token estava exposto no código-fonte mesmo.)

A rotina do http server

O servidor é iniciado na porta 8080 e eu passo um handler que faz a mão de aceitar a requisição vindo de outra origem. Eu separei a aplicação em duas imagens Docker onde o backend roda na porta 8080 e o frontend roda na porta 80.

Enquanto eu fazia a aplicação, eu tive problemas de CORS quando tentava fazer a requisição a partir do meu frontend. A requisição feita diretamente pela porta 8080 acontecia normalmente, enquanto o navegador encontrava problemas com isso.

O mecânismo CORS suporta requisições seguras do tipo cross-origin e transferências de dados entre navegadores e servidores web.

Esse servidor ainda usa uma handle function que faz a gestão das rotas. No caso eu criei uma rota /search onde a partir dela eu chamo outra função que é a responsável por continuar a requisição e posteriormente isso vai me retornar um JSON.

Parte 2: Frontend
O frontend foi feito com HTML, CSS, JavaScript, Bootstrap e o bom e velho jQuery.

Como eu queria testar alguns filtros no front, eu implementei o Datatables, um recurso que eu já conhecia de outros carnavais e que sempre me ajudou na hora de criar tabelas dinâmicas no frontend e ele tem como dependência o jQuery.

Eu poderia entrar a fundo nos conceitos de frontend e ficar explicando o motivo de eu não ter usado React, Vue ou qualquer outro super poder de frontend, porém, como eu falei, o desafio era primeiro implementar e fazer funcionar e depois, quem sabe, um dia, eu refaça o frontend usando alguma biblioteca do momento.

Image Print do Frontend da aplicação

Aqui você encontra uma base do HTML usado no projeto:

<!DOCTYPE html>
<html lang="pt-br">
<head>
    <meta charset="UTF-8">
    <meta content="width=device-width, initial-scale=1.0" name="viewport">
    <title>AquiPertin - Encontre tudo o que você precisa, onde você precisa.</title>

    <!-- Bootstrap -->
    <link crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css"
          integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" rel="stylesheet">

    <!-- Datatables -->
    <link href="https://cdn.datatables.net/1.10.25/css/dataTables.bootstrap4.min.css" rel="stylesheet">

    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css">
    <style>
        div#contentResultTable {
            width: 80%;
        }
    </style>
</head>
<body>
<div class="content vh-100">
    <header>
        <div class="container">
            <nav class="navbar navbar-light pt-5">
                <a class="navbar-brand" href="#">Aqui<strong>Pertin</strong></a>
                <span class="navbar-text">_simples assim :)</span>
            </nav>
            <span class="navbar-text">Encontre tudo o que você precisa.</span>
        </div>
    </header>
    <div class="container">
        <div class="row align-items-center">
            <div class="mx-auto">
                <div class="text-justify font-weight-bold">
                    <form class="mt-5" id="myForm">
                        <div class="form-group">
                            <label for="query"></label>
                            <input class="form-control" id="query" name="query" placeholder="Ex: Restaurantes próximos"
                                   type="text">
                            <button class="btn btn-primary mt-3" type="submit">Consultar</button>

                        </div>
                        <div class="form-group">
                            <input type="checkbox" id="onlyOpen" name="onlyOpen" checked>
                            <label for="onlyOpen" class="mt-3">Mostrar somente lugares abertos agora</label>
                        </div>

                    </form>
                </div>
            </div>
        </div>
    </div>

    <div aria-hidden="true" class="modal fade" id="reviewsModal" role="dialog" tabindex="-1">
        <div class="modal-dialog reviews-modal-dialog">
            <div class="modal-content">
                <div class="modal-header">
                    <h5 class="modal-title">Avaliações</h5>
                    <button class="close" data-dismiss="modal" onclick="closeModals()" type="button">&times;</button>
                </div>
                <div class="modal-body" id="reviewsContent"></div>
                <div class="modal-footer">
                    <button class="btn btn-secondary" data-dismiss="modal" onclick="closeModals()" type="button">Fechar</button>
                </div>
            </div>
        </div>
    </div>
    <!-- Modal -->
    <div aria-hidden="true" aria-labelledby="hoursModalLabel" class="modal fade" id="hoursModal" tabindex="-1">
        <div class="modal-dialog">
            <div class="modal-content">
                <div class="modal-header">
                    <h5 class="modal-title" id="hoursModalLabel">Horários de Funcionamento</h5>
                    <button class="close" data-dismiss="modal" onclick="closeModals()" type="button">&times;</button>
                </div>
                <div class="modal-body">
                    <!-- O conteúdo do horário de funcionamento será inserido aqui pelo JavaScript -->
                </div>
                <div class="modal-footer">
                    <button class="btn btn-secondary" data-dismiss="modal" onclick="closeModals()" type="button">Fechar</button>
                </div>
            </div>
        </div>
    </div>

    <div class="container-fluid mt-4" id="contentResultTable" style="display: none;">
        <div class="row">
            <div class="col-md-12">
                <table class="table table-striped table-bordered mt-5" id="resultTable">
                    <thead>
                    <tr>
                        <th title="Nome da empresa">Nome</th>
                        <th title="Endereço da empresa">Endereço</th>
                        <th title="Localização no Google Maps">Mapa</th>
                        <th title="Número de telefone da empresa">Telefone</th>
                        <th title="Avaliações do usuário">Avaliações</th>
                        <th title="Website da empresa">Site</th>
                        <th title="Classificação média dos usuários">Nota</th>
                        <th title="Horário de funcionamento da empresa">Funcionamento</th>
                    </tr>
                    </thead>
                    <tbody>
                    <!-- As linhas com os dados da requisição serão adicionadas aqui pelo JavaScript -->
                    </tbody>
                </table>
            </div>
        </div>
    </div>
</div>
<!-- jQuery -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>

<!-- Datatables JS -->
<script src="https://cdn.datatables.net/1.10.25/js/jquery.dataTables.min.js"></script>
<script src="https://cdn.datatables.net/1.10.25/js/dataTables.bootstrap4.min.js"></script>

<!-- Bootstrap JS Bundle -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"></script>

<!-- App JS -->
<script src="search.js"></script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Abaixo eu compartilho com você um JS onde eu me dei ao trabalho de comentar quase que linha a linha para explicar o que eu fiz.

// armazena os dados recuperados
let table;
let dataStore = {};
// este código é executado quando o documento HTML é completamente carregado
$(document).ready(function() {
    // inicia o plugin DataTables na tabela #resultTable
    table = $('#resultTable').DataTable({
        // configura a linguagem em português para o DataTable
        "language": {
            "url": "https://cdn.datatables.net/plug-ins/1.10.24/i18n/Portuguese-Brasil.json"
        },
        // recupera uma instância DataTable existente, se aplicável
        "retrieve": true,
        // destrói qualquer instância DataTable existente para inicializar uma nova
        "destroy": true
    });
    // ativa tooltips para quaisquer elementos com um atributo de título
    $('[title]').tooltip();
});
// gera o conteúdo HTML a ser adicionado ao modal de revisão
function generateModalContent(reviewData) {
    return reviewData.filter(review => review.text)
        .map(review => {
            console.log(review.rating);
            let rating = parseFloat(review.rating) || 0;
            let fullStars = '<i class="fas fa-star"></i>'.repeat(Math.floor(rating));
            let halfStar = ((rating - Math.floor(rating)) !== 0 ) ? '<i class="fas fa-star-half-alt"></i>' : '';
            return `<h5>${review.author_name} ${fullStars}${halfStar}</h5>
                    <p>${review.text}</p>`;
        })
        .join('');
}
// abre um modal, o conteúdo é gerado dependendo do parâmetro 'type'
function openModal(index, type) {
    let modalContent;
    let modal;
    switch (type) {
        case 'reviews':
            modalContent = generateModalContent(dataStore[index].result.reviews);
            modal = $('#reviewsModal');
            break;

        case 'hours':
            modalContent = dataStore[index].result.opening_hours.weekday_text.map(hours => `<p>${hours}</p>`).join('');
            modal = $('#hoursModal');
            break;
    }
    modal.find('.modal-body').html(modalContent);
    modal.modal('show');
}
// fecha todos os modals abertos
function closeModals() {
    $('#reviewsModal').modal('hide');
    $('#hoursModal').modal('hide');
}
// gera o conteúdo HTML para resultados de busca
function createResultHtml(data) {
    // filtra os resultados de acordo com a opção "Apenas abertos"
    if (document.getElementById("onlyOpen").checked) {
        data = data.filter(item => item.result.opening_hours && item.result.opening_hours.open_now);
    }
    // ordena os resultados de acordo com a opção "Ordenar por"
    data.sort((a, b) => {
        // se a opção for "Avaliações", ordena por avaliações
        let a_open = (a.result.opening_hours && a.result.opening_hours.open_now) ? 1 : 0;
        let b_open = (b.result.opening_hours && b.result.opening_hours.open_now) ? 1 : 0;

        // se a opção for "Apenas abertos", ordena por abertos
        if (a_open && !b_open) {
            return -1;
        }
        if (!a_open && b_open) {
            return 1; // a_open false, b_open true, move a down
        }
        // se a opção for "Avaliações", ordena por avaliações - a ser implementado
        let a_rating = a.result.rating || 0;
        let b_rating = b.result.rating || 0;
        return b_rating - a_rating;
    });

    // armazena os dados para uso posterior
    dataStore = data;

    // gera o HTML para cada resultado
    return data.map((item, index) => {
        // se o local não tiver avaliações, o valor padrão é 0
        const rating = item.result.rating !== undefined ? item.result.rating : 0;
        // gera o HTML para as estrelas de avaliação
        const fullStars = '<i class="fas fa-star"></i>'.repeat(Math.floor(rating));
        const halfStar = ((rating - Math.floor(rating)) !== 0 ) ? '<i class="fas fa-star-half-alt"></i>' : '';
        const ratingHTML = `${fullStars}${halfStar}`;
        // gera os detalhes do local
        let details = {
            // se o valor for indefinido, o valor padrão é "Not Available"
            name: item.result.name || 'Not Available',
            address: item.result.formatted_address || 'Not Available',
            phoneNumber: item.result.formatted_phone_number || 'Not Available',
            reviews: item.result.reviews && Array.isArray(item.result.reviews) ? item.result.reviews.length : 0,
            website: item.result.website || "Not Available",
            url: item.result.url || "Not Available",
            rating: ratingHTML,
            // se o local estiver aberto, o valor padrão é "Open", caso contrário, "Closed"
            isOpen: item.result.opening_hours && item.result.opening_hours.open_now
                ? "<span class='badge bg-success'><i class='fas fa-arrow-up'></i> Open</span>"
                : "<span class='badge bg-danger'><i class='fas fa-arrow-down'></i> Closed</span>"
        };
        // retorna a linha da tabela HTML
        return getTableRow(details, index);
    }).join(""); // une todas as linhas em uma única string
}
// gera uma linha de tabela HTML a partir de detalhes de um local
function getTableRow(details, index){
    // retorna a linha da tabela HTML usando template literals
    return `
      <tr>
            <td>${details.name}</td>
            <td>${details.address}</td>
            <td><a href="${details.url}" target="_blank">Ver link</a></td>
            <td><a href="tel:${details.phoneNumber}">${details.phoneNumber}</a></td>
            <td><a href="#" onclick="openModal(${index}, 'reviews')">Ver avaliações (${details.reviews})</a></td>
            <td><a href="${details.website}" target="_blank">Acessar site</a></td>
            <td>${details.rating}</td>
            <td><a href="#" onclick="openModal(${index}, 'hours')">${details.isOpen}</a></td>
      </tr>
   `;}
// adiciona um listener de evento para o formulário de pesquisa
document.getElementById("myForm").addEventListener("submit", function(event) {
    event.preventDefault();
    // recupera o valor do campo de entrada - principal valor de pesquisa do App
    const query = document.getElementById("query").value;
    document.getElementById("contentResultTable").style.display = "block";
    // destrói a tabela existente antes de atualizar os dados usando o plugin DataTables
    table.destroy(); // primeiro destruir a tabela
    // faz uma solicitação (promise) para o servidor backend usando o valor de pesquisa
    fetch(`http://localhost:8080/search?query=${query}`, { method: 'GET' })
        // converte a resposta em JSON
        .then(response => response.json())
        // atualiza a tabela com os dados recuperados
        .then(data => {
            // atualiza o conteúdo da tabela com os dados recuperados
            document.getElementById("resultTable").querySelector("tbody").innerHTML = createResultHtml(data);
            // inicia o plugin DataTables na tabela #resultTable
            table = $('#resultTable').DataTable({
                "language": {
                    "url": "https://cdn.datatables.net/plug-ins/1.10.24/i18n/Portuguese-Brasil.json"
                },
                "retrieve": true
            });
        })
        // captura quaisquer erros e os imprime no console
        .catch((error) => console.error('Error:', error));
});
// adiciona um listener de evento para o botão "Limpar"
let placeholders = ["Ex: Restaurante na Paulista", "Ex: Mecânica em Curitiba ", "Ex: Café no aeroporto de Guarulhos", "Ex: Cinema em Porto Alegre", "Ex: Ponto de táxi em Copacabana", "Ex: Rodoviária em Fortaleza", "Ex: Museus em BH", "Ex: Farmácias no centro", "Ex: Supermercados próximos"];
let index = 0;
// altera o placeholder do campo de entrada a cada 2 segundos
function alterarPlaceholder() {
    let inputElement = document.getElementById("query");
    inputElement.placeholder = placeholders[index];
    index++;
    if (index >= placeholders.length) {
        index = 0;
    }
}
// adiciona um listener de evento para o "onload" do objeto window
window.onload = function() {
    setInterval(alterarPlaceholder, 2000);
};
Enter fullscreen mode Exit fullscreen mode

Anatomia da aplicação

Esta aplicação web é uma combinação de HTML, CSS, JavaScript e usa uma API Rest em Go para buscar lugares próximos baseado na consulta do usuário.

HTML: O código HTML estrutura o layout do site com elementos de entrada de usuário, um formulário para enviar query, um checkbox para mostrar apenas lugares abertos agora, botões de ação, etc.

Ele também inclui área onde os resultados da API da busca serão mostrados em uma tabela.

Além disso, são incluídos os modais (janelas de diálogos) para exibir as avaliações e os horários de funcionamento dos lugares.

CSS: Os estilos CSS foram incluídos para melhorar a aparência do site. Nesse caso, é usada principalmente para ajustar a largura da tabela de resultados e para estilizar a tabela. O restante são classes do Bootstrap que já organizam todo o layout.

JavaScript: É usado para manipular as interações e ações do usuário, enviar solicitações à API, receber respostas e manipulá-las e atualizar a interface do usuário de acordo.

Especificamente, o código JavaScript faz o seguinte:

  • Inicia criando a tabela de resultados com DataTable, uma biblioteca JQuery que adiciona funcionalidades de interação à tabelas em HTML.
  • Define uma função generateModalContent que cria o conteúdo HTML a ser exibido nos modais de avaliações.
  • Define funções openModal e closeModals para controlar a exibição dos modais de avaliações e horários de funcionamento.
  • Define a função createResultHtml para criar as linhas da tabela de resultados. Esta função recebe os dados da API, filtra se o usuário marcou para ver só locais abertos agora, organiza os locais considerando se está aberto e a nota de avaliação do local.
  • A função getTableRow retorna um template string que é a linha da tabela de resultados.
  • Adiciona um listener de evento "submit" ao formulário. Quando o formulário é submetido, a consulta de entrada do usuário é capturada e uma solicitação é enviada à API. Quando a resposta é recebida da API, a tabela é preenchida com os dados da resposta.

Por fim, a função alterarPlaceholder (que nome tosco) altera o placeholder do input de consulta a cada 2 segundos, dando exemplos ao usuário de como usar a aplicação.

O código JavaScript está usando o AJAX (com a função fetch) para realizar requisições HTTP assíncronas à API do servidor, e então, usa os dados recebidos para atualizar dinamicamente a interface de usuário.


Em resumo, neste App as consultas são feitas à API REST desenvolvida em Go (backend) e os resultados são processados e apresentados na forma de uma tabela no navegador do usuário (frontend).

Este App é um exemplo simples de como uma API Restful pode ser utilizada para buscar e exibir dados dinamicamente no frontend de uma aplicação web.


Considerações finais

O desafio do projeto foi concluído com sucesso.

Se você chegou até aqui, muito provavelmente você percebeu que o App poderia ter sido feito seguindo uma série de boas práticas no desenvolvimento de software.

A minha justificativa para isso é que este era um ambiente de testes construído para errar, ou seja, o meu objetivo era cometer erros durante o processo e me colocar em situações onde eu precisaria buscar uma solução para um problema que não tinha sido previsto anteriormente.

Se este App fosse aproveitado para algo realmente em produção, muita coisa precisaria ser feita, como começar escrevendo todas as funções a partir de testes, aplicar os conceitos de arquitetura e código limpos, organizar o projeto de forma física e lógica aplicando boas práticas de desenvolvimento.

Apenas a critério de conhecimento, eu comecei a escrever a requisição HTTP só para testar o endpoint e quando eu vi tinham se passado 6 horas!

Eu cometi erros como por exemplo não ir fazendo commits, ou pior ainda, nem tinha criado um repositório Git e já tinha horas de código.

Outro erro de noob foi iniciar o repo com sono e cansado e acabar expondo o token da API na variável de ambiente e commitar isso!

Outro erro ainda mais tosco foi eu não ter feito um mock do payload do Google para implementar o frontend. Eu fiquei testando em produção, fazendo requisições na API do Google a cada teste que eu fazia 😂

O Google cobra por SKU, ou seja, a cada informação que eu busco eles me cobram. E tem informações que custam mais caro do que outras, e algumas ainda existem uma cobrança à parte quando o consumo fica dentro de um determinado range.

O resultado dessa brincadeira consumindo dados de uma API em produção foi de R$ 726,29. Obviamente que eu usei um voucher do Google Cloud para testar a aplicação e não precisei pagar nenhum real por isso.

Image Custo do GCP

Eu ainda tive que lidar com HTML e CSS que há muitos anos eu não colocava a mão de verdade. Javascript eu ainda sabia como fazer algumas coisas e onde procurar ajuda quando precisasse.

Eu também tive que estudar sobre Docker, CORS, requisições http a partir do resultado de uma outra requisição http, etc, etc.

No fim das contas eu considero que o resultado foi duca e certamente eu vou criar mais implementações assim, sempre testando serviços reais que podem ser usados em aplicações reais.

Se você gostou deste artigo, considere deixar um comentário. Eu vou ficar feliz em saber a tua opinião sobre tudo isso.

Até a próxima.

Top comments (0)