Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introdução

Seja bem-vindo a Programação B, um livro a ser usado na disciplina da UFS de mesmo nome. Essa disciplina busca ensinar ao alunos os principais conceitos e técnicas utilizados no desenvolvimento de aplicações, em particular aplicações web e mobile.

A maioria das pessoas que possuem alguma formação em computação acaba trabalhando na área de desenvolvimento. É, de longe, a sub-área de computação que mais emprega pessoas.

A razão é simples: existe uma variedade imensa de problemas, que interessam a um ou mais clientes. Um cliente pode estar interessado numa solução para a área de veterinária, por exemplo. Outro pode estar interessado numa solução para a área de infraestrutura rodoviária. São assuntos completamente diferentes. Não é possível que exista uma única aplicação que satisfaça a todos os clientes. Portanto, múltiplas aplicações acabam surgindo, adaptadas aos requisitos de um certo grupo de clientes. Devido a essa variedade, existe uma demanda muito grande por profissionais que possam construir tais aplicações. Nosso objetivo é aprender como construir tais aplicações.

Do ponto de vista do currículo de computação, a utilidade dessa disciplina pode ser percebida ao olhar para as disciplinas de POC (Prática Orientada a Computação), que consistem em atender a demanda de um cliente real através da criação de um produto real, com algum grau de inovação. A maioria dos clientes em POC costumam ser profissionais de uma área fora da computação, e a maioria das soluções tendem a tomar a forma de uma aplicação web e/ou mobile. Alguns alunos chegam à disciplina de POC sem conheciemnt dos conceitos e técnicas de desenvolvimento de aplicações, e sem experiência prática nas ferramentas que ajudam nesse processo, tendo que obtê-los “na marra”, durante a disciplina, ao mesmo tempo que lida com o aprendizado de outros conceitos de produto, inovação e gerenciamento de projeto.

Com a ascensão da IA generativa, muitos acreditam que não faz mais sentido estudar conceitos de desenvolvimento, pois a IA pode fazer o desenvolvimento em nome da pessoa. O aluno que tem essa visão deve reconsiderar: a IA, apesar de facilitar radicalmente a codificação e a interpretação de volumes significativos de código, não é uma bala de prata. A qualidade do código gerado por IA é diretamente proporcional ao conhecimento que o usuário tem do domínio do problema. A resolução de problemas específicos e/ou que exigem algum conhecimento do produto ainda precisam de muito esforço por parte do usuário para que a IA entregue um código de alta qualidade, no nível do desejado pelo usuário. De maneira geral, o usuário que pede para a IA fazer X ou Y sem entender ao certo como X ou Y funcionam por baixo está condenado ao fracasso.

Finalmente, espero que o aluno encare este livro não como um manual de receitas a serem decoradas, mas como um convite a entender por que cada técnica existe. Ao longo do curso, nenhum conceito é apresentado antes de a aplicação que construímos sentir a falta dele. Quando você entende o problema que uma técnica resolve, passa a saber não só como usá-la, mas quando não usá-lá. Tal discernimento é essencial.

O que este livro assume

Este livro assume que será lido de maneira sequencial. Capítulos posteriores são construídos em cimas de capítulos anteriores.

Este livro assume que o leitor tem conhecimento intermediário em programação. Este não é um livro de introdução à programação. O leitor deve estar familiarizado com conceitos de programação no geral, e de conceitos dos principais paradigmas de programação (imperativo, funcional e orientado a objetos).

Esse livro assume que o desenvolvimento será feito em um ambiente Linux ou similar (macOS, WSL, etc). Conceitos de linha de comando (terminal) serão introduzidos e utilizados ao longo do livro, porém sem muito detalhamento. Portanto, é desejável que o usuário tenha alguma familiariedade com CLIs. Caso sinta alguma dificuldade para entender os trechos de código a serem digitados no terminal, recomendo que acesse o Command line crash course da MDN.

Além disso, o livro assume que o usuário sabe ler a documentação técnica de linguagens, bibliotecas, etc. Esse tipo de documentação, inclusive, costuma existir somente em inglês.

Como usar este livro

Este livro busca ensinar os conceitos e técnicas de desenvolvimento de aplicações de maneira genérica e agnóstica. Ou seja, queremos que o conhecimento seja transferível para qualquer tipo de aplicação, linguagem ou framework que o aluno venha a usar. Isso não significa que não usaremos uma linguagem ou framework(s) específicos ao longo do curso. Usaremos principalmente TypeScript/JavaScript e os frameworks dessa linguagem. No entanto, o usuário está livre para implementar os exemplos e exercícios em qualquer linguagem (Python, C#, Kotlin, Swift, etc) ou framework (FastAPI, .NET, Spring, Angular, Svelte, etc) que preferir.

O aprendizado gira em torno de uma aplicação de exemplo, o TamoJunto, uma plataforma comunitária de eventos que construímos de forma incremental, capítulo a capítulo. Em vez de montar todo o backend, depois todo o frontend web e por fim todo o mobile, avançamos por fatias verticais: cada nova funcionalidade atravessa o banco de dados, o servidor e as interfaces web e mobile de uma só vez. É assim que aplicações reais são construídas, e é assim que aprenderemos a construí-las.

Introdução ao Desenvolvimento de Aplicações

Tudo nasce com um problema, uma necessidade de alguém. Pode ser algo essencial, como um problema na área de saúde, ou não, como a necessidade de ouvir músicas.

Então, os desenvolvedores devem construir aplicações que resolvam esses problemas e satisfaçam essas necessidades. A área da computação que se preocupa em entender os conceitos e técnicas relevantes na construção dessas aplicações é conhecida como Desenvolvimento de Aplicações. Esse capítulo busca dar um visão geral sobre essa área.

O que é uma aplicação?

O que é uma aplicação?

Uma aplicação é um software (um programa, ou conjunto de programas) criado para executar tarefas para um usuário, ao contrário de softwares que existem para gerenciar o próprio computador (como um sistema operacional, um driver de dispositivo ou um compilador). A palavra “aplicação” deriva justamente dessa ideia: a aplicação do poder computacional a um problema humano.

Certas propriedades diferenciam uma aplicação de outros tipos de software:

  1. Orientado a domínio. Uma aplicação serve para resolve um problema em algum domínio: gerenciar as finanças, editar fotos, reservar passagens aéreas, conversar com os amigos.

  2. User-facing. Uma aplicação é uma fronteira de interação. Um usuário humano abre, utiliza e fecha uma aplicação. É o que distingue uma aplicação de uma biblioteca ou serviço de software. Uma biblioteca pode ser usada por uma aplicação; um serviço atua como o motor por trás de uma aplicação; mas a aplicação é a “coisa” que o usuário percebe e interage.

  3. Composição. Aplicações modernas raramente são compostas de um único programa. Invés disso, elas são um conjunto de programas interagindo entre si: um site na web, um aplicativo em um dispositivo móvel, um servidor por trás, um banco de dados, etc. Todos esses formam, em conjunto, a aplicação.

    Esses programas comunicam-se entre si utilizando protocolos de redes de computadores. O protocolo mais relevante em aplicações modernas é o protocolo HTTP, que será abordado em Noções de Redes de Computadores.

Figura 1-1: Os vários programas que compõem uma aplicação moderna, conectados via protocolos de rede.

Interfaces de Usuário

Interfaces de Usuário

Como aplicações são user-facing, é necessário que exista alguma forma do usuário-fim emitir comandos para a aplicação. Isto é, é necessário que exista alguma interface de usuário (User Interface ou UI). Uma interface de usuário pode ser implementada de várias formas.

Graphical User Interfaces (GUIs)

A forma mais comum de implementar uma interface de usuário é através de menus, telas e cliques em botões.

A maioria das aplicações que são utilizadas pelo usuário comum, aquele com pouca formação em computação, são classificadas como GUIs (Graphical User Interfaces). Isto é, são aplicações cuja a interface é gráfica, composta de menus, telas, cores, botões, etc. Essas GUIs podem ser acessadas a partir de vários meios ou plataformas. As principais plataformas hoje em dia são:

  • Desktop. São aplicações que são abertas diretamente num computador desktop (PC, laptop) , por meio de algum sistema operacional, como o Windows, Linux ou macOS.

    Exemplos: Spotify Desktop, Whatsapp Desktop, Google Chrome, Microsoft Edge, Explorador de Arquivos, Gerenciador de Tarefas, Steam Desktop, etc.

  • Mobile. São aplicações muito semelhantes às desktop mas que, ao invés de serem abertas em um dispositivo desktop, são abertas em dispositivos móveis, como smartphones e tablets, que rodam sistemas operacionais móveis, como Android e iOS.

    Exemplos: aplicativo do Spotify no Android, Whatsapp no Android, Whatsapp no iOS, etc.

    Aplicações para desktop ou mobile são conhecidas como aplicações nativas, pois rodam diretamente acima do sistema operacional.

  • Web. São aplicações acessadas por meio de um site, utilizando um navegador web (Google Chrome, Mozilla Firefox, Microsoft Edge, Safari).

    Exemplos: Spotify Web, Whatsapp Web, SIGAA, etc.

    Diferentemente das aplicações nativas, as aplicações web não rodam diretamente acima do sistema operacional, mas sim por meio de um navegador web. Note que quem atua como aplicação nativa nesses casos é o o navegador web.

Figura 1-2: As três plataformas de aplicações gráficas: desktop (nativa), mobile (nativa) e web (via navegador).

Command-Line Interfaces (CLIs)

Enquanto as GUIs são projetadas para o usuário comum, com pouco conhecimento em computação, existem aplicações que são projetadas para usuários com um conhecimento maior em computação. Essas aplicações, ao invés de utilizarem menus, telas e botões, utilizam comandos de texto, digitados em um terminal.

O principal benefício de aplicações CLIs é que, diferentemente das GUIs, elas não escondem as funcionalidades por trás de vários cliques em menus, telas e botões. Dessa forma, é muito fácil de automatizar a interação com aplicações CLI.

Como exemplos dessas aplicações, temos ffmpeg, diskutil, diskpart, ssh, zip, unzip, rsync, brew, winget, grep, find, ping, entre outros.

Por exemplo, veja como um usuário pode usar a CLI do ffmpeg para converter um vídeo no formato .mkv para .mp4:

ffmpeg -i video.mkv video.mp4

Figura 1-3: Comparação entre GUI e CLI para a mesma tarefa: converter um vídeo com ffmpeg.

Apesar de CLIs serem muito úteis, o foco desse livro é em aplicações gráficas (GUIs), em particular nas plataformas web e mobile.

Componentes da Aplicação

Componentes da Aplicação

Aplicações são compostas de vários programas que comunicam-se entre si. Esses programas/componentes recebem nomes específicos a depender do papel que eles desempenham na aplicação como um todo.

  • O frontend (também chamado de cliente ou client-side) é o componente da aplicação que implementa a interface que o usuário interage. O frontend pode ser implementado como um site web (frontend web), um aplicativo de dispositivo móvel (frontend mobile), um aplicativo de dispositivo desktop (frontend desktop) ou uma CLI.

    O código do frontend é executado no dispositivo de cada cliente/usuário da aplicação. Se uma aplicação possui 10 usuários simultâneos, o código do frontend está sendo executado em 10 dispositivos ao mesmo tempo.

  • O backend (também chamado de servidor ou server-side) é o componente da aplicação que implementa as regras de negócio por trás da interface. Isso é feito através da interação com o banco de dados, validação de regras específicas do domínio (ex: um pedido não pode exceder R$ 5.000 sem aprovação do gerente), controles de segurança e integrações com outros serviços.

    O backend geralmente é implementado como uma API (Application Programming Interface), uma interface programática (isto é, acessada por meio de código e/ou protocolos de redes) que é acessada pelo frontend dos vários clientes.

    O código do backend, diferente do frontend, não é executado no dispositivo de cada cliente, mas sim em um ou mais servidores. Um servidor é um computador dedicado à execução de serviços e aplicações, configurado pela pessoa ou organização que oferece a aplicação.

  • O banco de dados (também chamado de camada de persistência) é o componente da aplicação que armazena os dados da aplicação e é responsável pela execução de consultas e outros comandos sobre esses dados.

    Diferentemente do frontend e do backend, o banco de dados é um software que não costuma ser implementado pelo desenvolvedor da aplicação. Invés disso, o desenvolvedor utiliza um banco de dados já implementado.

    Existem bancos de dados relacionais (baseados em tabelas de registros com esquema fixo) e não-relacionais (baseados em vários outros modelos, como documentos flexíveis ou grafos). Alguns exemplos de bancos de dados: Postgres, MariaDB, MySQL, Oracle, MS-SQL, MongoDB, Neo4j, ScyllaDB, etc.

Figura 1-4: O Spotify como exemplo de aplicação em 3 camadas: múltiplos frontends, um backend com API, e um banco de dados.

A maioria das aplicações modernas possuem pelo menos esses 3 componentes. Esse tipo de aplicação é conhecida como aplicação em 3 camadas.

Aplicações mais antigas não possuíam uma distinção rígida entre as camadas de frontend e backend. Ao invés disso, o programa que implementava a interface de usuário (quase sempre uma aplicação desktop) interagia diretamente com o banco de dados, formando assim uma aplicação em duas camadas.

Aplicações mais complexas geralmente dividem seu backend em microserviços, cada um funcionando como um backend isolado que oferece uma API. Elas também envolvem outros componentes, como filas, tópicos, brokers, caches, balanceadores de carga, proxy reverso, API gateways, service meshes, engines de busca, object storage, schedulers/job runners, service discovery e observabilidade.

Capítulo 2: Noções de Redes de Computadores

Hoje em dia, a base de quase qualquer aplicação que você imaginar, seja ela web, mobile ou desktop, é o protocolo de rede HTTP (HyperText Transfer Protocol). Compreender ele é essencial para entender como as aplicações que construíremos funcionam por baixo.

Se você não faz ideia do que seja um “protocolo”, não tem problema. Esse capítulo define conceitos fundamentais de redes de computadores, no nível que é necessário para o nosso curso.

Redes de Computadores

Redes de Computadores

Os conceitos que iremos falar aqui podem ser vistos em qualquer curso ou livro de Redes de Computadores. Redes é uma sub-área de computação dedicada a entender como computadores, separados fisicamente por alguma distância, trocam dados entre si. É uma das partes mais importantes de um currículo de computação, pois o mundo moderno inteiro depende dos conceitos estudados nessa área.

Só para te dar uma noção, vamos ver como redes de computadores acabam surgindo no seu dia:

  1. Seu celular toca o alarme e te acorda.
  2. Você abre o Whatsapp para ver as mensagens. O seu celular usa conceitos de redes para obter as mensagens que foram enviadas para você. Ele inicia uma comunicação com os computadores (chamados aqui de “servidores”) do Whatsapp/Meta.
  3. Você pede uma corrida na Uber. O seu dispositivo precisa se comunicar com os servidores da Uber. E os dispositivos dos motoristas também precisa se comunicar com os servidores da Uber. Eles se comunicam usando conceitos de redes.
  4. Você encontra uma corrida. Tanto o dispositivo do passageiro quanto do motorista enviam continuamente informações para o servidor da Uber, contendo suas respectivas coordenadas. O servidor da Uber lê essas informações e as exibe no aplicativo para os dois usuários envolvidos na corrida: você e o seu motorista.
  5. Você abre o Youtube ou a Netflix. Seu dispositivo precisa obter os dados que compõem os vídeos (áudio, imagens, legendas, etc) de algum lugar. Para isso, ele se comunica com o servidor do Youtube/Netflix, para então exibir os vídeos no seu dispositivo. Eles se comunicam usando conceitos de redes.
  6. Você compartilha um arquivo via Bluetooth com um amigo de sala. Para isso funcionar, é preciso que os dois dispositivos se comuniquem entre si, no que chamamos de “rede sem fio”, utilizando apenas ondas eletromagnéticas. Eles se comunicam usando conceitos de redes.

Acho que deu pra ter uma noção da importância dessa disciplina para a sociedade moderna.

Conceitos básicos de redes

Conceitos básicos de redes

A figura abaixo ilustra uma rede de computadores simples, com quatro hosts conectados entre si.

Figura 2-1: Quatro hosts conectados formando uma rede de computadores.

Rede

Uma rede de computadores, ou simplesmente rede, nada mais é do que um conjunto de computadores que, mesmo separados fisicamente, podem trocar informações entre si.

Por “computadores”, eu quero dizer qualquer dispositivo de computação: Desktops, celulares, consoles de videogame, Smart TVs, carros, etc. Muitas vezes, esses computadores que participam da rede são chamados de hosts.

Existem redes privadas, que são gerenciadas e acessadas por uma organização, e redes públicas, que são acessíveis por múltiplas organizações. A rede pública mais importante de todas é a Internet.

A troca de informações numa rede não pode ser feita de qualquer forma. Para dois computadores trocarem informações, é importante que ambos falem a mesma língua e sigam as mesmas regras, o que nos leva ao próximo conceito.

Protocolo

Um conceito que está presente em toda a disciplina de redes é o de protocolo: um conjunto de regras que duas ou mais partes devem seguir fielmente para atingir um objetivo comum. Para um protocolo funcionar é necessário que todas as partes estejam cientes dele e o sigam.

Um protocolo de rede é um protocolo onde as partes são computadores! E as regras dos protocolos de rede geralmente seguem mais ou menos a mesma cara, geralmente fazendo alguma dessas funções:

  • Definem quais são os tipos de mensagens possíveis que as partes podem trocar
  • Definem qual o conteúdo das mensagens trocadas.
    • Como as partes são computadores, esse conteúdo é sempre em binário (1s e 0s). Então uma mensagem possível é 01110110001 que pode representar um OLÁ, por exemplo.
  • Definem o fluxo válido das mensagens.
  • Definem os erros e/ou situações de ajuste, e como proceder.

Em redes, temos vários protocolos, cada qual com um objetivo específico. Por exemplo, alguns protocolos que são usados hoje em dia em várias aplicações são:

  • O SMTP (Simple Mail Transfer Protocol), que é o protocolo que permite envio e transmissão de e-mails entre computadores. É o que o Gmail, Outlook, entre outros usam quando precisam enviar um e-mail.
  • O BitTorrent, que é o protocolo que permite a transmissão de torrents entre computadores. É o que aplicações como qBittorrent, µTorrent, entre outros usam.
  • O DNS (Domain Name System), que é o protocolo que permite que nossos celulares e computadores consigam traduzir um nome como sigaa.ufs.br, que só faz sentido para humanos, para algo que possa ser entendido por máquinas.

Os protocolos são definidos em documentos de texto, conhecidos como RFCs (Request for Comments). Por exemplo, o SMTP é definido na RFC 5321, enquanto o DNS é definido na RFC 1035.

Camadas

Os protocolos não são todos iguais. Isso ocorre pois certos protocolos ficam responsáveis por apenas algumas partes da comunicação, enquanto outros ficam responsáveis por outras.

Por exemplo, considere o caso em que o protocolo SMTP seja utilizado pelo servidor do Gmail para enviar um e-mail para o servidor do Outlook.

Não faz sentido que a RFC do SMTP responda as seguintes perguntas:

  • Como deve ser feita a transmissão física dos bits entre dois computadores? Será usado fio de cobre, fibra ótica ou tranmissão sem-fio (ondas de rádio). Qual o nível de voltagem que será usado para representa um bit 0 e um bit 1? Ou que deverá ser feito se acontecer alguma interferência eletromagnética na hora de transferir esses sinais elétricos. Essas são preocupações físicas, sem relação direta com troca de emails.
  • Como o hardware de um dispositivo deve distinguir entre os sinais elétricos destinados para diferentes conexões num mesmo computador? Quando os sinais elétricos de uma conexão terminam e o de outra começam? Como garantir que os dados sejam direcionados para o dispositivo correto, dentro de uma rede local? Essas são preocupações de enlace, sem relação direta com troca de emails.
  • Qual rota de computadores deve ser seguida para um certa conexão? Como diferenciar globalmente dois computadores? Como redes de diferentes organizações/empresas devem se conectar? Essas são preocupações a nível de redes em si, sem relação direta com troca de emails.
  • Como as conexões são representadas no sistema operacional de cada dispositivo? Como associar um processo do sistema operacional a uma conexão de rede? Devemos garantir que o processo do computador de destino recebeu a última mensagem? Se sim, como? Como regular a velocidade da transmissão para evitar que a qualidade da rede seja degradada? Essa são preocupações de transporte, sem relação direta com troca de emails.

Nenhuma dessas preocupações dizem respeito a troca de emails, mas todas devem ser respondidas antes que qualquer email seja enviado. Além disso, essas preocupações não devem ser respondidas apenas para a troca de emails, mas também para outros tipos de aplicações, como transferência de arquivos, navegar pela web, DNS, BitTorrent, etc.

De fato, cada uma dessas preocupações se encaixa em uma camada.

Figura 2-2: As 5 camadas do modelo TCP/IP, da mais baixa (física) à mais alta (aplicação).

Uma camada de rede é o conjunto de preocupações e responsabilidades que podem ser resolvidas em conjunto, por um mesmo protocolo.

Elas recebem o nome de camadas pois certas responsabilidades precisam ser resolvidas anteriormente, criando uma hierarquia ou camada de responsabilidades. Um protocolo numa camada \( L \) precisa que um protocolo na camada \( L - 1 \) já tenha resolvido os problemas sob sua responsabilidade. De maneira geral, dizemos que a camada \( L - 1 \) provê serviços para a camada \( L \).

Modelos de Rede: OSI e TCP/IP

Um modelo de rede é uma forma específica de definir quantas e quais são as camadas de rede. Existem dois modelos de rede: o modelo OSI e o modelo TCP/IP.

O modelo OSI é utilizado em alguns contextos acadêmicos e a sua numeração (de 1 a 7) é a que as pessoas geralmente utilizam para se referir as camadas. Já o modelo TCP/IP é o que é de fato implementado na Internet e utilizado na prática.

Abaixo, descrevemos as 5 camadas do modelo TCP/IP, a partir da mais básica (baixo nível):

  1. Camada Física
    • Camada que se preocupa em como será feita a transmissão física dos sinais que carregam os dados na rede. É onde são projetados os padrões dos meios de transmissão, como cabos, ondas de radio-frequência, etc.
    • Principais protocolos dessa camada: IEEE 802.3 (redes cabeadas, também conhecido como Ethernet), IEEE 802.11 (redes sem fio do tipo Wi-fi), IEEE 802.15 (redes sem fio do tipo Bluetooth). Importante notar que esses protocolos cobrem ambas a camada física e de enlace, pois essas possuem uma dependência muito grande entre si.
  2. Camada de Enlace
    • Camada que se preocupa no nível lógico imediatamente acima da transmissão física, em particular na conexão ponto-a-ponto (o enlace ou link) entre dois hosts.
    • Principais protocolos dessa camada: IEEE 802.3 (redes cabeadas, também conhecido como Ethernet), IEEE 802.11 (redes sem fio do tipo Wi-fi), IEEE 802.15 (redes sem fio do tipo Bluetooth).
  3. Camada de Rede
    • Camada que se preocupa em definir o escopo de uma rede e como será feita a comunicação entre duas redes. Define um endereço lógico para cada host chamado endereço IP (Internet Protocol). É esse endereço que é utilizado para rotear as comunicações entre os hosts.
    • Principais protocolos dessa camada: IPv4, IPv6, ICMP, ICMPv6, etc.
  4. Camada de Transporte
    • Camada que se preocupa na comunicação fim-a-fim entre dois processos do sistema operacional de cada host.
    • Principais protocolos dessa camada: TCP, UDP, SSL/TLS (baseado no TCP), QUIC (baseado no UDP).
  5. Camada de Aplicação
    • Camada que se preocupa em definir o funcionamento das aplicações de rede, que entram em contato direto com o usuário. Troca de emails, envio de arquivos, impressão e navegação web são exemplos de “aplicações” aqui, pois representam as ações que o usuário-fim quer realizar.
    • Principais protocolos dessa camada: DNS, DHCP, HTTP, SMTP, FTP, WebSockets, BitTorrent.

Endereço de Rede/Endereço IP

Dentro de uma rede de computadores, é necessário que um participante dela (um host) saiba como identificar e diferenciar uma conexão que deve ser direcionada para o host A de outra que deve ser direcionada para o host B. Para resolver esse problema, foram criados os endereços de rede.

Cada host numa rede é identificado pelo seu endereço de rede. Um endereço de rede é simplesmente uma sequência de números que pode ser interpretada para identificar hosts.

No modelo TCP/IP, o endereço de rede segue o formato dos protocolos da camada de rede: IPv4 (Internet Protocol version 4) e/ou IPv6 (Internet Protocol version 6). Nesses casos, um nome alternativo para o endereço de rede é “endereço IP” ou apenas “IP”.

Vamos ver um exemplo, seguindo o formato do protocolo IPv4:

  • Um host A tem o endereço de rede 10.10.3.47. Ele está se comunicando com dois outros hosts: B (cujo endereço é 10.10.3.3) e C (cujo endereço é 10.10.5.98).
  • Sempre que A quiser mandar uma mensagem para B, ele manda um pacote cujo endereço de destino é 10.10.3.3. C não é o destino dessa mensagem.

Figura 2-3: Host A usa o endereço IP de destino para direcionar pacotes para B ou C.

Note que se um mesmo host está conectado em duas ou mais redes distintas (como um roteador que roteia pacotes entre duas ou mais redes), ele terá mais de um endereço IP.

Existem dois endereços IP especiais, chamados de endereços de loopback, que podem ser usados por um computador para se referir a ele mesmo. Um é o IPv4 127.0.0.1. O outro é o IPv6 ::1. Esses endereços são úteis na fase de desenvolvimento, onde executamos uma aplicação na nossa própria máquina local. Dessa forma, podemos nos comunicar com a aplicação através dos endereços de loopback, simulando um host externo.

Serviços de Rede

Enquanto na camada de rede os hosts de origem e destino são representados por meio de endereços IP, na camada imediatamente acima, a camada de transporte, um mesmo host pode oferecer vários serviços de rede. Um serviço de rede é um programa cuja entrada é obtida a partir de requisições de outros hosts (chamados de clientes) que chegam por meio de uma rede de computadores.

Ou seja, um serviço de rede é um processo do sistema operacional que tem a capacidade de escutar por requisições que vêm de uma rede de computadores e responder a essas requisições.

Cada serviço de rede executa algum protocolo da camada de aplicação que os dois lados da comunicação, tanto servidor (o host que executa/oferece o serviço de rede) quanto os clientes (os hosts remotos que mandam requisições e solicitam o serviço de rede), precisam saber falar.

Portas

Da mesma forma que podemos ter múltiplos processos em execução no sistema operacional, podemos ter múltiplos serviços de rede em execução. Portanto, o sistema operacional precisa distinguir o tráfego de rede que vai para um serviço de rede \( A \) do tráfego que vai para outro serviço de rede \( B \). Ou seja, o sistema operacional precisa demultiplexar1 o tráfego de rede.

A maneira pela qual o SO faz essa demultiplexação é utilizando portas de rede. Cada porta de rede é um par (Protocolo de Transporte, Número de Porta) que identifica a qual serviço de rede um certo tráfego pertence. Nesse par:

  • Protocolo de Transporte é um dos dois protocolos básicos da camada de transporte: TCP (Transmission Control Protocol) ou UDP (User Datagram Protocol).

    O TCP é um protocolo orientado à conexão, o que significa que ele garante que um lado da comunicação saiba que as mensagens enviadas de fato chegaram ao destino e na ordem correta. Além disso, ele possui alguns controles para garantir a qualidade da rede. No entanto, é um protocolo que possui mais overhead (sobrecarga), isto é, custo computacional.

    Já o UDP é um protocolo não-orientado à conexão, o que significa que os participantes da comunicação não tem garantia sobre se as mensagens chegaram na ordem correta ou se elas sequer chegaram. Para compensar, é um protocolo com consideravelmente menos overhead.

  • Número de Porta é um número de 16 bits (vai de 0 a 65535). Esse número serve para permitir que, em um mesmo computador, múltiplos serviços de rede de um mesmo protocolo de transporte possam estar em execução em um dado momento.

Cada serviço de rede em execução num dado host é mapeado para uma porta de rede, e a porta atua como um endpoint de comunicação. Por exemplo, num mesmo computador/host podemos ter 3 serviços de rede em execução:

  • O serviço de rede do Spotify para fazer streaming de música usando a porta TCP/4070.
  • O serviço de rede para enviar emails (protocolo SMTP) usando a porta TCP/25.
  • O serviço de rede para transferir arquivos (protocolo FTP) usando a porta TCP/21.

Figura 2-4: O SO usa portas para direcionar o tráfego ao serviço de rede correto.

Certas portas são chamadas de portas bem-conhecidas (well-known ports), pois geralmente são usadas para os serviços de rede (protocolos) mais comuns. Essas portas usam números que vão de 0 a 1023. Por exemplo, a porta TCP/80 é sempre utilizada para o protocolo HTTP. A porta TCP/443 é sempre utilizada para o protocolo HTTPS. A porta UDP/53 é muito usada para o protocolo DNS. E por aí vai.

Hostname

Apesar dos computadores utilizarem endereços IP, é muito mais fácil para um humano lembrar de mail.google.com do que lembrar do IP 142.250.219.37. Portanto, todo host costuma ter um hostname, que é o nome legível do host, além de um IP. Por exemplo, suponha que uma organização tenha os seguintes hosts:

  • Um host para o seu portal web. O hostname do host é www e o IP é 10.100.3.2.
  • Um host para o seu serviço de e-mail. O hostname do host é mail e o IP é 10.100.3.3.
  • Um host para o seu serviço de API2. O hostname do host é api e o IP é 10.100.3.4.

Domínio

Para agrupar os hostnames de todos os hosts que pertencem a uma mesma organização ou estrutura, é usado um domínio: uma sequência de nomes (legíveis por humanos), cujas partes são separadas por pontos.

Geralmente, a parte final de um domínio é o chamado de TLD (Top-Level Domain), que é comum a várias organizações que compartilham uma mesma característica. Então, por exemplo:

  • O domínio .ufs.br é utilizado pela organização UFS. Seu TLD é .br, geralmente usado por organizações do Brasil.
  • O domínio .globo.com é utilizado pela organização Globo. Seu TLD é .com, que vem de company, geralmente usado por organizações empresariais. Note que mesmo sendo uma organização brasileira, o domínio da Globo não está no TLD .br.

FQDN

Sempre que alguém quer se referir a um host específico de uma organização, evitando qualquer ambiguidade, ele utiliza o FQDN (Fully Qualified Domain Name) do host, que nada mais é do que a junção de hostname + domínio.

Figura 2-5: Um FQDN é a junção de hostname + domínio. O TLD é a parte final do domínio.

O FQDN também pode ser usado para obter o endereço IP do host. Alguns exemplos:

  • O FQDN www.google.com refere-se ao host que oferece o serviço do site do Google.
  • O FQDN g1.globo.com refere-se ao host que oferece o serviço g1 da Globo.
  • O FQDN ge.globo.com refere-se ao host que oferece o serviço ge da Globo.

DNS

Lembre-se que os computadores não trabalham com hostnames, domínios ou FQDNs, mas sim com endereços IP. Portanto, sempre que digitamos algo como www.google.com, o computador precisa traduzir esse FQDN para um endereço IP.

Essa tradução (ou resolução) de nomes só é possível graças ao protocolo DNS (Domain Name System). Sempre que digitamos www.google.com, o DNS entra em ação e traduz esse FQDN para algum endereço IP (exemplo: 142.250.219.34).

Figura 2-6: O cliente consulta o DNS para obter o IP e então conecta-se ao servidor.


  1. Demultiplexar é o processo de separar um único canal de comunicação em múltiplos canais. É o inverso da multiplexação: combinar diferentes canais em um único canal. ↩

  2. Se você não sabe o que é uma API, não se preocupe: descobriremos no capítulo 3. ↩

O Protocolo HTTP

O Protocolo HTTP

Vimos que aplicações são compostas de vários componentes que se comunicam entre si: frontend (web e/ou mobile), backend, banco de dados, etc. Apesar de não ser uma regra, hoje em dia praticamente toda aplicação realiza a comunicação entre frontend e backend utilizando um protocolo da camada de aplicação chamado HTTP (HyperText Transfer Protocol).

Modelo Cliente-Servidor

Sempre que temos uma conversa que segue o protocolo HTTP, são definidos dois papéis essenciais:

  • O servidor HTTP: É um host que fica sempre escutando por novas conexões. Ele recebe requisições dos clientes e responde elas. Por exemplo, imagine que você acessa o SIGAA em http://sigaa.ufs.br/. Nesse caso, o servidor HTTP é o próprio sigaa.ufs.br (este é o FQDN do host que executa, ou hospeda, o SIGAA).
  • O cliente HTTP: É o host que inicia novas conexões. É ele quem manda as requisições para o servidor e fica aguardando pela resposta dele. No exemplo anterior, quando você acessa o SIGAA em http://sigaa.ufs.br/, você é o cliente HTTP. Note que é possível que dois ou mais clientes HTTP diferentes conectem-se ao mesmo servidor HTTP.

Ou seja, o protocolo HTTP segue o chamado Modelo Cliente-Servidor. A imensa maioria dos protocolos da camada de aplicação seguem esse modelo1.

Figura 2-7: O cliente inicia a conexão e envia requisições; o servidor aguarda e responde.

Mensagens HTTP: Requisições e Respostas

Toda conversa entre o cliente e o servidor é uma sequência de mensagens HTTP. Uma mensagem HTTP pode ser de dois tipos: uma Requisição HTTP (enviada pelo cliente para o servidor) ou uma Resposta HTTP (enviada pelo servidor para o cliente).

Figura 2-8: Ciclo HTTP: o cliente envia uma requisição e o servidor retorna uma resposta.

Anatomia de uma Requisição HTTP

Toda comunicação HTTP começa por uma requisição, pois é o cliente quem inicia a comunicação. O servidor fica passivamente aguardando novas conexões iniciadas pelos clientes.

Vamos primeiro ver um exemplo de requisição HTTP2. A requisição abaixo é de um cliente que está acessando uma página web sobre um relatório do clima.

Figura 2-9: Partes de uma requisição HTTP: request line (método, path, query, versão), headers e payload.

GET /blog/2026/08/relatorio-clima.html?page=2&sort=date&highlight=temperatura HTTP/1.1
Host: www.guardian.co.uk
Accept: text/html
Accept-Language: pt-BR
User-Agent: Chrome/127.0.0.0
Cookie: session_id=a3f8b2c1e9d04716; _ga=GA1.2.1847263950.1723641200; consent=accepted
Connection: keep-alive

Vamos entender parte por parte:

  • A primeira linha é a linha de requisição (request line). Ela contém 4 partes:

    • Um método (GET, especificando que quer obter/ver algum conteúdo).
    • Um path (/blog/2026/08/relatorio-clima.html, especificando a localização do conteúdo que será visto).
    • Uma query string (?page=2&sort=date&highlight=temperatura, especificando parâmetros para mudar/personalizar o conteúdo retornado).
    • A versão do protocolo HTTP (HTTP/1.1, especificando quais funcionalidades HTTP estão disponíveis).
  • As próximas 6 linhas são cabeçalhos (headers) HTTP. Cada cabeçalho tem um nome (ex: Host) e um valor (ex: www.guardian.co.uk) separados por :.

    Os cabeçalhos HTTP permitem configurar a conexão e manter valores (estado) ao longo dela.

    • O header Host: www.guardian.co.uk especifica o host sendo acessado3.
    • Accept: text/html especifica qual o tipo esperado do conteúdo. Nesse caso, é esperado um conteúdo de texto que forma um documento HTML. Veremos mais a frente o que é um documento HTML.
    • Accept-Language: pt-BR especifica que o conteúdo deve ser retornado preferencialmente em Português do Brasil.
    • User-Agent: Chrome/127.0.0.0 indica que o cliente acessou o servidor por meio do navegador Google Chrome na versão 127.0.0.0. O valor desse header muda a depender do programa usado para acessar o site, podendo ser desde navegadores até ferramentas CLI como curl.
    • Cookie: session_id=a3f8...; _ga=GA1.2...; consent=accepted é o header que armazena os famosos cookies do site. Esse header guarda 3 cookies:
      • session_id é o ID de uma sessão específica do usuário naquele site. É por meio desse cookie que é possível fechar um site e, ao reabrí-lo, ainda estarmos conectados na nossa conta.
      • _ga é um cookie usado pelo Google Analytics, um serviço da Google que os sites contratam para poder rastrear e analisar o tráfego dos usuários, coletando dados como páginas visualizadas, duração da sessão, localização geográfica, tipo de dispositivo, etc.
      • consent é um cookie usado para identificar se um usuário consentiu com os termos de uso do site, como os cookies que são utilizados para rastrear o comportamento do usuário. É relevante principalmente em países que possuem legislação sobre uso de dados, como a LGPD.
    • Connection: keep-alive especifica que o clienter quer re-utilizar a mesma conexão TCP, re-aproveitando as configurações já feitas.
  • Após os headers pode vir uma ou mais linhas que formam o payload (ou corpo) da requisição. Como trata-se de uma requisição GET, não há necessidade de payload.

Método

Cada requisição HTTP possui um método (também chamado de verbo). O método HTTP representa que tipo de ação aquela requisição pretende realizar. Os 5 principais métodos HTTP são:

  • GET: Representa a ação de exibir um conteúdo ou listar os conteúdos existentes.
  • POST: Representa a ação de criar um conteúdo.
  • PUT: Representa a ação de substituir/modificar um conteúdo por completo.
  • PATCH: Representa a ação de substituir/modificar partes de um conteúdo.
  • DELETE: Representa a ação de remover um conteúdo.

As requisições que usam os métodos POST, PUT e PATCH costumam possuir uma payload (corpo da requisição), contendo os dados sobre o que será criado/modificado.

Veremos mais a frente como o padrão REST utiliza esses métodos para representar o gerenciamento de recursos.

Path

Uma requisição HTTP sempre diz respeito a algum conteúdo ou recurso. O path (caminho) da requisição indica o caminho daquele conteúdo/recurso em específico.

Por exemplo:

  • O path / é geralmente usado para representar a raiz daquele site, e geralmente retorna a página inicial do site.
  • O path /blog pode ser usado para representar todas as postagens do blog de um determinado site.
  • O path /blog/2026 pode ser usado para representar todas as postagens do blog do ano de 2026.
  • O path /blog/2026/08/relatorio-clima.html pode ser usado para representar uma postagem em específico do blog. Note que o path termina em .html, indicando que esse recurso trata-se de um arquivo/documento HTML.
  • O path /blog/imagens/banner.png representa uma imagem em específico do blog. Note que o path termina em .png, indicado que esse recurso trata-se de uma imagem PNG.
  • O path /api/usuarios/42 representa o recurso do tipo usuário cujo ID é igual a 42.

Query String

Dado um path, é possível que queiramos customizar o conteúdo retornado. Essas customizações normalmente são especificadas na parte da requisição chamada query string (também chamada de query parameters).

Geralmente, essas customizações costumam ser:

  • Qual filtro de busca utilizar
  • Qual página dos resultados deve ser retornada
  • Se os resultados devem ser ordenados, crescentemente ou decrescentemente.

A query string é uma lista de pares parâmetro=valor, separador por &. A lista de query parameters sempre começa com ?. Por exemplo, veja a query string do exemplo, ?page=2&sort=date&highlight=temperatura. Temos 3 parâmetros:

  • page=2 especifica que deve ser retornada a segunda página de resultados.
  • sort=date especifica que os resultados devem vir ordenados pela data.
  • highlight=temperatura especifica que o campo temperatura deve ter destaque.

Nem toda requisição tem uma query string. Ela é opcional.

A junção do path + query params formam o alvo da requisição.

Um dos benefícios dos query params é que como eles fazem parte do alvo da requisição, eles podem ser salvos e uma dada consulta (com seus filtros e opções) pode ser salvo e reutilizado. Por exemplo, o seguinte pode ser colado na barra de navegação para levar a uma consulta do Google pelo termo ufs: https://www.google.com/search?q=ufs. Veja que é utilizado o path /search e a query string ?q=ufs.

Versão HTTP

Toda requisição HTTP inclui a versão do protocolo que será utilizada. Atualmente, existem 4 versões em uso:

  • HTTP/1.0: Versão definida na RFC 1945. Estabeleceu os fundamentos do protocolo. Alguns servidores antigos ainda utilizam essa versão.
  • HTTP/1.1: Definida na RFC 2616. Estabeleceu otimizações importantes, como conexões persistentes (Connection: keep-alive). É a versão mais comum de ser vista na Internet.
  • HTTP/2: Protocolo deixou de ser baseado em texto e se transformou num protocolo binário. Funcionalidades: Multiplexação de várias mensagens sobre um mesmo canal de comunicação, compressão de cabeçalhos, priorização de arquivos, server push, etc.
  • HTTP/3: Versão mais recente, ainda pouco utilizada. Funcionalidades: troca do protocolo TCP pelo protocolo QUIC, criptografia obrigatória, mais rápido, etc.

Apesar disso, você só lerá requisições mencionando as duas primeiras versões, pois elas são baseadas em texto, já o HTTP/2 e o HTTP/3 são protocolos binários (não são legíveis por humanos).

Headers

Os campos anteriores juntos formam a linha de requisição (request line) de uma requisição HTTP. A request line é a primeira linha da requisição. Alguns exemplos de request lines:

  • GET /index.html HTTP/1.1
  • GET /search?q=openai&lang=en HTTP/1.1
  • POST /api/usuarios HTTP/1.1

Além da request line, uma requisição HTTP também possui os cabeçalhos (headers). Os headers são pares Nome: Valor e servem para adicionar metadados e configurações para a comunicação em si.

Alguns exemplos de headers relevantes:

  • Host. Especifica qual o FQDN do host que atua como servidor HTTP3. Esse é o único header de requisição que é obrigatório.

    Exemplo: Host: www.exemplo.com.

  • Cookie. É o header cujo valor são os famosos cookies do site. Esses cookies são utilizados para guardar informações que persistem ao longo de várias requisições HTTP (estado). Cada cookie é um par nome=valor, separados por ;.

    Exemplo: Cookie: session_id=abc123def456; theme=dark; lang=pt-BR, com 3 cookies.

  • Referer. É o header utilizado quando um site te redireciona para outro, e o seu valor é a URL do site que solicitou o redirecionamento.

    Exemplo: Referer: https://www.example.com/products?category=shoes.

Os headers vêm após a request line. Cada header é escrito em uma linha.

Payload

Quando a requisição HTTP usa métodos como POST, PUT e PATCH, ela inclui um payload (também chamado de corpo da requisição).

Por exemplo, considere a seguinte requisição, que cria uma nova usuária em um sistema:

POST /api/users HTTP/1.1
Host: example.com
Content-Type: application/json
Content-Length: 68

{"name":"Alice Morrow","email":"alice@example.com","age":29,"active":true}

Vamos entender por partes:

  • A request line é POST /api/users HTTP/1.1. Portanto, ela irá incluir um payload. Após a request line, seguem 3 headers.

    1. Host: example.com é o host que atua como servidor HTTP.
    2. Content-Type: application/json é um header que especifica qual o tipo do conteúdo que está no payload. Nesse caso, application/json indica que o payload segue o formato JSON, que veremos mais a frente.
    3. Content-Length: 68 é um header que especifica o tamanho do payload em bytes.
  • Após os 3 headers, temos uma linha em branco, que serve para indicar que a lista de headers acabou e o payload virá em seguida.

  • O conteúdo do payload é:

    {"name":"Alice Morrow","email":"alice@example.com","age":29,"active":true}
    

    Os dados desse payload contêm o nome, email, idade e o status de ativação da nova usuária.

Anatomia de uma Resposta HTTP

Toda requisição HTTP precisa ser respondida pelo servidor. Uma resposta HTTP tem uma estrutura mais simples que as requisições.

Suponha que fizemos uma requisição da forma GET /index.html HTTP/1.1. É uma requisição para a página inicial de algum site. A resposta que o servidor HTTP entregou foi:

HTTP/1.1 200 OK
Date: Sun, 16 Aug 2026 18:37:12 GMT
Server: Nginx/1.25.2
Content-Type: text/html; charset=utf-8
Content-Length: 171
Connection: keep-alive

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Página Inicial</title>
</head>
<body>
    <h1>Seja bem-vindo!</h1>
    <p>A página carregou com sucesso.</p>
</body>
</html>

Vamos analisar por partes:

  • A primeira linha é a linha de resposta (response line), contendo:
    • A versão do protocolo (HTTP/1.1)
    • O status code (200)
    • A reason phrase (OK), uma texto legível explicando o status code. Um status code sempre terá a mesma reason phrase. 200 OK = sucesso.
  • Segue-se uma linha para cada cabeçalho de resposta (response header). Perceba que alguns headers também podem aparecer em requisições:
    • Date: Sun, 16 Aug 2026 18:37:12 GMT indica o horário que o servidor mandou a resposta.
    • Server: Nginx/1.25.2 indica qual o programa ou biblioteca utilizado pelo servidor para enviar respostas HTTP.
    • Content-Type: text/html; charset=utf-8 indica que o corpo da resposta vem no formato HTML e foi codificado utilizando o padrão UTF-8.
    • Content-Lenght: 171 indica que o corpo da resposta tem 171 bytes.
    • Connection: keep-alive indica que deve ser re-utilizada a mesma conexão TCP.
  • Uma linha em branco indica o fim da lista de headers e o início do corpo da resposta.
  • Seguem 171 bytes que formam o corpo da resposta. A resposta é um document no formato HTML que será exibido para o usuário, montando a estrutura e o conteúdo da página inicial.

Status Code e Reason Phrase

O servidor irá indicar o tipo de resultado usando o status code, que é um código de 3 dígitos. O status code pode indicar se foi falha, falha por erro do cliente, falha por erro do servidor, redirecionamento, timeout, etc).

Todo status code está associado a um texto legível chamado reason phrase, dando mais detalhes do que aconteceu para além do código numérico.

Os status codes podem ser classificados em categorias de acordo com o primeiro dígito:

CategoriaTipoExemplos
1xxInformação100 Continue
101 Switching Protocols
2xxSucesso200 OK
201 Created
3xxRedirecionamento301 Moved Permanently
302 Found
4xxErro do Cliente400 Bad Request
403 Forbidden
404 Not Found
5xxErro do Servidor500 Internal Error
502 Bad Gateway
503 Service Unavailable

Você pode consultar todos os status codes, seus significados e dicas de como usá-los na referência da MDN: Códigos de status de respostas HTTP.

Headers

Assim como as requisições, as respostas HTTP possuem headers para carregar metadados e configurar a conexão.

Alguns headers são exclusivos das respostas, como Server. Já outros podem ser usados tanto por requisições como por respostas, como Connection, Content-Type e Content-Length.

Body/Corpo

A parte mais importante de uma resposta HTTP é o corpo (body). É nele que está o conteúdo que o cliente está interessado. Esse conteúdo geralmente é:

  • Um documento HTML, que define a estrutura e o conteúdo de uma página web.
  • Uma arquivo de mídia (imagem, áudio, vídeo)
  • Dados no formato XML ou JSON

O corpo da resposta pode ser vazio somente em algumas ocasiões, como em resposta a uma requisição DELETE.


  1. Apesar da maioria dos protocolos da camada de aplicação seguirem o Modelo Cliente-Servidor, existem alguns protocolos que seguem outro modelo, chamado de Modelo Peer-to-Peer (P2P), onde cada host atua como um misto de cliente e servidor, falando em pé de igualdade com outros hosts. O exemplo clássico é o protocolo BitTorrent, mas também existem outros como o protocolo Bitcoin e XMPP/Jabber. ↩

  2. Para simplificar, o exemplo mostrado omite os caracteres que representam as quebras de linhas (\r\n) numa requisição. Esses caracteres são importantes para indicar quando uma seção da requisição termina e outra começa. ↩

  3. Os mais atentos podem estar se perguntando qual a necessidade do header Host se, para iniciar a comunicação HTTP, o cliente precisa saber antecipadamente qual o host que atuará como servidor. O motivo é que o header Host é utilizado para implementar uma técnica conhecida como virtual hosting, onde um único host físico, com um único IP, hospeda várias aplicações HTTP diferentes (chamados de hosts virtuais). O host físico que atua como servidor olha o valor desse header para saber qual aplicação/host virtual o cliente está interessado.

    Inclusive, é por esse motivo que o header Host é o único que é obrigatório. Uma requisição sem ele será rejeitada pelo servidor. ↩ ↩2

E o HTTPS?

E o HTTPS?

Praticamente todos os sites que você acessa hoje na web começam com https://. Esses sites usam o protocolo HTTP junto com um protocolo de segurança chamado SSL/TLS (Secure Socket Layer/Transport Layer Security). Esse protocolo criptografa os dados entre cliente e servidor, impedindo que um hacker malicioso leia os dados que estão trafegando.

Conclusão e Exercícios

Conclusão

Por hora, terminamos nosso estudo de redes. A parte mais importante desse capítulo para o restante do livro é o protocolo HTTP, pois este é o protocolo que o frontend e o backend de uma aplicação costumam usar para se comunicar.

Exercícios

  1. Abra um terminal (no Windows, aperte Win + R, escreva cmd e aperte Ok) e digite o seguinte comando:

    (Obs: não precisa escrever C:\>, é apenas para indicar que deve ser digitado no Prompt de comando)

    C:\> curl -i sigaa.ufs.br
    

    Esse comando irá mostrar uma mensagem HTTP. Responda:

    1. O tipo da mensagem é uma resposta ou requisição HTTP?
    2. Qual o status code?
    3. Quantos e quais os headers?
    4. Essa mensagem tem corpo? Se sim, qual seu tamanho em bytes?
    5. O que o corpo diz? Como você mudaria o comando para atender o que o corpo diz?
  2. No seu computador, abra um navegador web (Chrome, Firefox, Edge, Brave) e abra uma nova aba. Aperte F12 (ou Ctrl + Shift + I), irá aparecer um painel com várias informações. Na parte superior desse painel, tem várias abas. Clique na aba chamada Rede/Network. Essa aba mostra as mensagens HTTP que seu navegador faz enquanto você navega na web.

    Então, sem fechar esse painel, digite o seguinte na barra de navegação: example.com e aperte Enter.

    Responda:

    1. Quantas linhas apareceram?
    2. Clique na primeira linha. No novo painel aberto, vá na aba Headers/Cabeçalhos. Quantos headers tem a primeira requisição? Qual o método da primeira requisição? Qual o status code da primeira resposta?
    3. No mesmo painel, determine se a primeira requisição teve uma payload não-vazia. Faça o mesmo para a resposta.
  3. Acesse https://httpbin.org/forms/post no seu navegador. Abra novamente o painel de Rede/Network. Preencha o formulário e envie. Observe a nova mensagem que apareceu após você clicar para enviar. A requisição tem corpo? Se sim, qual seu tamanho? E a resposta?

Ambiente de Desenvolvimento

Imagine a seguinte cena: um grupo de 4 alunos precisa entregar um projeto de software para uma disciplina da faculdade. Cada aluno ficou responsável por uma parte do código. O prazo está apertado. Eles combinaram de trocar as partes pelo WhatsApp.

O primeiro aluno manda o arquivo projeto.zip. O segundo baixa, faz suas alterações, e manda de volta projeto_v2.zip. O terceiro, que não viu a mensagem do segundo, fez suas alterações na versão do primeiro e manda projeto_atualizado.zip. Agora ninguém sabe qual é a versão mais recente. O quarto aluno tenta juntar tudo à mão, num domingo à noite, e estraga uma função que já estava funcionando. Resultado: projeto_final_v3_AGORA_VAI.zip.

Se você já passou por algo parecido, sabe que o problema não é competência. É a falta de uma ferramenta adequada para coordenar o trabalho de várias pessoas sobre os mesmos arquivos. Essa ferramenta existe e se chama controle de versão.

Esse capítulo apresenta os conceitos e práticas de controle de versão utilizando o Git, a ferramenta mais utilizada no mundo para esse fim. Depois, veremos como organizar um projeto com vários componentes (backend, frontend web, frontend mobile) dentro de um único repositório.

Controle de Versão

Controle de Versão

Um sistema de controle de versão (Version Control System ou VCS) é um software que registra as alterações feitas em arquivos ao longo do tempo. Ele resolve três problemas fundamentais:

  1. Histórico. Você consegue ver exatamente o que mudou, quando mudou e quem mudou. Se algo quebrar, é possível voltar a uma versão anterior.
  2. Colaboração. Várias pessoas podem trabalhar nos mesmos arquivos ao mesmo tempo sem sobrescrever o trabalho umas das outras.
  3. Rastreabilidade. Cada conjunto de alterações vem acompanhado de uma mensagem explicando o motivo da mudança. Meses depois, é possível entender por que aquele trecho de código existe.

Existem vários sistemas de controle de versão: CVS, Subversion (SVN), Mercurial e Git. Hoje, o Git é o padrão da indústria. Foi criado em 2005 por Linus Torvalds (o mesmo criador do Linux) para gerenciar o código-fonte do kernel Linux, que já naquela época era um projeto com milhares de contribuidores.

Instalando e Configurando o Git

Instalando e Configurando o Git

Antes de tudo, instale o Git:

  • Linux (Debian/Ubuntu):
    sudo apt install git
    
  • macOS:
    brew install git
    
  • Windows: Instale o Git for Windows. Ele inclui o Git Bash, um terminal que emula o ambiente Linux. Se você usa o WSL (recomendado neste livro), instale normalmente via apt dentro do WSL.

Após a instalação, configure seu nome e e-mail. Essas informações ficam gravadas em cada alteração que você registrar:

git config --global user.name "Maria Silva"
git config --global user.email "maria@example.com"

Você pode verificar a configuração com:

$ git config --global --list
user.name=Maria Silva
user.email=maria@example.com

Repositório

Repositório

Um repositório (ou repo) é um diretório cujo histórico de alterações é gerenciado pelo Git. Para transformar um diretório qualquer em um repositório Git, usamos o comando git init:

$ mkdir meu-projeto
$ cd meu-projeto
$ git init
Initialized empty Git repository in /home/maria/meu-projeto/.git/

A partir desse momento, o Git passa a monitorar esse diretório. Ele cria uma pasta oculta chamada .git/ que armazena todo o histórico. Você nunca precisa mexer diretamente nessa pasta.

O Ciclo Básico: Staging e Commit

O Ciclo Básico: Staging e Commit

O Git não registra alterações automaticamente. Você decide o que salvar e quando. O fluxo básico tem dois passos:

  1. Staging (preparação): você marca quais alterações deseja incluir no próximo registro.
  2. Commit (registro): você grava essas alterações no histórico, junto com uma mensagem descritiva.

Vamos ver na prática. Suponha que criamos um arquivo index.html dentro do nosso repositório:

<!DOCTYPE html>
<html>
<head>
    <title>Meu Site</title>
</head>
<body>
    <h1>Olá, mundo!</h1>
</body>
</html>

Se rodarmos git status, o Git nos diz que existe um arquivo novo que ele ainda não está rastreando:

$ git status
On branch main

No commits yet

Untracked files:
  (use "git add <file>..." to include in what will be committed)
	index.html

nothing added to commit but untracked files present (use "git add" to track)

Para incluir esse arquivo no próximo commit, usamos git add:

$ git add index.html

Agora o arquivo está na área de staging (staging area). Podemos confirmar com git status:

$ git status
On branch main

No commits yet

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)
	new file:   index.html

O Git está dizendo: “quando você fizer o próximo commit, vou incluir a criação do arquivo index.html”. Para gravar de fato, usamos git commit com uma mensagem que descreve a alteração:

$ git commit -m "Criar página inicial com título e saudação"
[main (root-commit) a1b2c3d] Criar página inicial com título e saudação
 1 file changed, 9 insertions(+)
 create mode 100644 index.html

Pronto. A alteração está registrada no histórico. O identificador a1b2c3d é um hash (resumo criptográfico) que identifica esse commit de forma única. Na prática, o hash completo tem 40 caracteres hexadecimais (ex: a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0), mas o Git permite usar apenas os primeiros caracteres, desde que não haja ambiguidade.

Vários Arquivos, Um Commit

Não é necessário fazer um commit por arquivo. Você pode adicionar vários arquivos à área de staging e registrá-los todos de uma vez. O importante é que cada commit represente uma unidade lógica de mudança: uma alteração coerente, com um propósito claro.

Por exemplo, se você criou um arquivo style.css e modificou o index.html para referenciá-lo, faz sentido que ambas as alterações entrem no mesmo commit, pois juntas elas formam uma unidade: “adicionar estilização à página inicial”.

$ git add index.html style.css
$ git commit -m "Adicionar estilização à página inicial"
[main e4f5a6b] Adicionar estilização à página inicial
 2 files changed, 18 insertions(+), 1 deletion(-)
 create mode 100644 style.css

Visualizando o Histórico

O comando git log mostra o histórico de commits, do mais recente ao mais antigo:

$ git log
commit e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3 (HEAD -> main)
Author: Maria Silva <maria@example.com>
Date:   Mon Aug 11 14:32:10 2026 -0300

    Adicionar estilização à página inicial

commit a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
Author: Maria Silva <maria@example.com>
Date:   Mon Aug 11 14:15:42 2026 -0300

    Criar página inicial com título e saudação

Cada entrada mostra o hash completo, o autor, a data e a mensagem. Para uma versão mais compacta, use git log --oneline:

$ git log --oneline
e4f5a6b Adicionar estilização à página inicial
a1b2c3d Criar página inicial com título e saudação

Vendo o que Mudou

Antes de fazer um commit, é boa prática verificar exatamente o que mudou. O comando git diff mostra as alterações que ainda não foram adicionadas à área de staging:

$ git diff
diff --git a/index.html b/index.html
index 3b18e51..f3c4a12 100644
--- a/index.html
+++ b/index.html
@@ -5,6 +5,7 @@
 </head>
 <body>
     <h1>Olá, mundo!</h1>
+    <p>Bem-vindo ao meu site.</p>
 </body>
 </html>

As linhas com + foram adicionadas; linhas com - foram removidas. Nesse caso, adicionamos um parágrafo de boas-vindas.

Branches

Branches

Até agora trabalhamos apenas em uma linha reta de commits. Mas imagine que você quer experimentar uma nova funcionalidade sem arriscar quebrar o que já está funcionando. Ou que dois colegas querem trabalhar em funcionalidades diferentes ao mesmo tempo. É para isso que existem os branches (ramos).

Uma branch é uma linha independente de desenvolvimento. Quando você cria uma branch, está criando um espaço isolado onde pode fazer commits sem afetar a branch principal.

A branch padrão geralmente se chama main1. Vamos criar uma branch nova para adicionar uma página “sobre”:

$ git branch pagina-sobre

Esse comando criou a branch, mas ainda estamos na main. Para mudar para a nova branch, usamos git checkout:

$ git checkout pagina-sobre
Switched to branch 'pagina-sobre'

Existe um atalho que cria a branch e muda para ela de uma vez:

$ git checkout -b pagina-sobre
Switched to a new branch 'pagina-sobre'

Agora, qualquer commit que fizermos será registrado na branch pagina-sobre, sem afetar a main. Vamos criar o arquivo sobre.html e fazer um commit:

$ git add sobre.html
$ git commit -m "Adicionar página sobre com informações do projeto"
[pagina-sobre 7d8e9f0] Adicionar página sobre com informações do projeto
 1 file changed, 12 insertions(+)
 create mode 100644 sobre.html

Se voltarmos para a main, o arquivo sobre.html não existe:

$ git checkout main
Switched to branch 'main'
$ ls
index.html  style.css

O arquivo só existe na branch pagina-sobre. As duas branches têm históricos diferentes a partir do ponto onde divergiram.

Merge

Quando a funcionalidade na branch está pronta e testada, queremos incorporá-la à branch principal. Esse processo se chama merge (mesclagem).

Para mesclar a branch pagina-sobre na main, primeiro vamos para a main e depois executamos o merge:

$ git checkout main
Switched to branch 'main'
$ git merge pagina-sobre
Updating e4f5a6b..7d8e9f0
Fast-forward
 sobre.html | 12 ++++++++++++
 1 file changed, 12 insertions(+)
 create mode 100644 sobre.html

Agora a main contém tudo o que estava em pagina-sobre. O Git disse “Fast-forward” porque não houve alterações na main enquanto trabalhávamos na outra branch. Ele simplesmente avançou o ponteiro.

Após o merge, a branch pagina-sobre pode ser apagada:

$ git branch -d pagina-sobre
Deleted branch pagina-sobre (was 7d8e9f0).

Conflitos

Nem todo merge é tão simples. Quando duas branches alteram a mesma parte do mesmo arquivo, o Git não consegue decidir qual versão manter. Isso é um conflito.

Suponha que dois desenvolvedores, Maria e João, criaram branches separadas a partir da main. Maria alterou a saudação no index.html para “Olá, pessoal!” na branch saudacao-maria. João alterou a mesma linha para “Bem-vindos ao site!” na branch saudacao-joao.

Maria faz o merge primeiro. Tudo corre bem. Agora João tenta fazer o merge da sua branch:

$ git checkout main
$ git merge saudacao-joao
Auto-merging index.html
CONFLICT (content): Merge conflict in index.html
Automatic merge failed; fix conflicts and then commit the result.

O Git nos avisa que houve um conflito. Se abrirmos o index.html, veremos algo assim:

<!DOCTYPE html>
<html>
<head>
    <title>Meu Site</title>
</head>
<body>
<<<<<<< HEAD
    <h1>Olá, pessoal!</h1>
=======
    <h1>Bem-vindos ao site!</h1>
>>>>>>> saudacao-joao
    <p>Bem-vindo ao meu site.</p>
</body>
</html>

O trecho entre <<<<<<< HEAD e ======= é a versão que já está no main (a alteração da Maria). O trecho entre ======= e >>>>>>> saudacao-joao é a versão da branch do João. Para resolver o conflito, editamos o arquivo e escolhemos a versão final. Podemos manter uma, a outra, ou combinar as duas:

    <h1>Olá, pessoal! Bem-vindos ao site!</h1>

Depois de editar, removemos os marcadores de conflito (<<<<<<<, =======, >>>>>>>), adicionamos o arquivo à staging area e fazemos o commit:

$ git add index.html
$ git commit -m "Resolver conflito: combinar saudações de Maria e João"
[main b3c4d5e] Resolver conflito: combinar saudações de Maria e João

Conflitos são normais. Sempre que duas pessoas mexem no mesmo trecho, o Git não tenta adivinhar qual versão é a correta. Ele mostra as duas e deixa a decisão com você.


  1. Historicamente, a branch padrão se chamava master. Em 2020, o GitHub e outras plataformas mudaram o padrão para main. Ambos os nomes funcionam da mesma forma. A escolha é apenas de convenção. ↩

Colaboração com Repositórios Remotos

Colaboração com Repositórios Remotos

Até agora, tudo aconteceu localmente, na sua máquina. Mas para colaborar com outras pessoas, é preciso que o repositório exista em algum lugar acessível por todos. Esse lugar é um repositório remoto (remote).

O serviço mais popular para hospedar repositórios remotos é o GitHub. Existem outros, como GitLab e Bitbucket, mas o GitHub é de longe o mais usado. Ao criar um repositório no GitHub, você obtém uma URL como https://github.com/maria/meu-projeto.git.

Clone

Para obter uma cópia local de um repositório que já existe no GitHub, usamos git clone:

$ git clone https://github.com/maria/meu-projeto.git
Cloning into 'meu-projeto'...
remote: Enumerating objects: 18, done.
remote: Total 18 (delta 4), reused 18 (delta 4), pack-reused 0
Receiving objects: 100% (18/18), done.
Resolving deltas: 100% (4/4), done.

Isso cria um diretório meu-projeto/ com todo o histórico de commits. O repositório clonado já vem configurado com um remote chamado origin, apontando para a URL de onde foi clonado.

Push e Pull

Depois de fazer commits localmente, você envia suas alterações para o remoto com git push:

$ git push origin main
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Writing objects: 100% (3/3), 312 bytes | 312.00 KiB/s, done.
Total 3 (delta 1), reused 0 (delta 0), pack-reused 0
To https://github.com/maria/meu-projeto.git
   e4f5a6b..b3c4d5e  main -> main

O comando git push origin main envia os commits da branch main local para o remote origin.

Para trazer as alterações que outros membros da equipe enviaram, usamos git pull:

$ git pull origin main
remote: Enumerating objects: 5, done.
remote: Total 5 (delta 2), reused 5 (delta 2), pack-reused 0
Unpacking objects: 100% (5/5), done.
From https://github.com/maria/meu-projeto.git
   b3c4d5e..f6a7b8c  main -> origin/main
Updating b3c4d5e..f6a7b8c
Fast-forward
 contato.html | 8 ++++++++
 1 file changed, 8 insertions(+)
 create mode 100644 contato.html

O git pull é equivalente a buscar as alterações do remoto (git fetch) e mesclá-las na branch local (git merge). Se houver conflitos, o processo de resolução é o mesmo que vimos antes.

O Fluxo de Trabalho Baseado em Branches

Na prática, equipes raramente fazem commits diretamente na main. O fluxo de trabalho mais comum segue estes passos:

  1. O desenvolvedor cria uma branch a partir da main para trabalhar numa funcionalidade específica (ex: adicionar-pagina-contato).
  2. Ele faz seus commits nessa branch.
  3. Quando a funcionalidade está pronta, ele envia a branch para o remoto (git push origin adicionar-pagina-contato).
  4. Ele abre um pull request (PR) no GitHub, que é uma proposta de incorporar as alterações dessa branch na main.
  5. Outros membros da equipe revisam o código, fazem comentários e sugerem mudanças.
  6. Após aprovação, o pull request é mesclado no main.

Esse fluxo tem duas vantagens importantes. Primeiro, a main nunca recebe código que não foi revisado por outra pessoa. Segundo, cada funcionalidade é desenvolvida de forma isolada, reduzindo a chance de conflitos.

.gitignore

Nem todo arquivo do seu projeto deve ser rastreado pelo Git. Arquivos gerados automaticamente (como node_modules/, que contém dependências baixadas), arquivos de configuração local do editor (como .vscode/settings.json) e, principalmente, arquivos com dados sensíveis (como senhas e chaves de API) não devem ir para o repositório.

Para dizer ao Git quais arquivos ignorar, criamos um arquivo chamado .gitignore na raiz do repositório:

node_modules/
dist/
.env
*.log

Cada linha define um padrão. Nesse exemplo:

  • node_modules/ ignora o diretório inteiro de dependências.
  • dist/ ignora a pasta de arquivos gerados pelo build.
  • .env ignora o arquivo de variáveis de ambiente (que costuma conter senhas).
  • *.log ignora qualquer arquivo com extensão .log.

O .gitignore em si deve ser commitado, pois todos os membros da equipe precisam ignorar os mesmos arquivos.

Organizando o Projeto: Monorepo

Organizando o Projeto: Monorepo

Em Introdução ao Desenvolvimento de Aplicações, vimos que uma aplicação é composta de vários componentes: backend, frontend web, frontend mobile, banco de dados. Cada um desses componentes tem seu próprio código, suas próprias dependências e seus próprios scripts de execução.

Uma pergunta natural é: cada componente deve ter seu próprio repositório, ou todos devem ficar juntos em um único repositório?

Monorepo vs. Multi-repo

Existem duas abordagens principais:

  • Multi-repo: cada componente vive em um repositório separado. O backend fica em github.com/equipe/backend, o frontend web em github.com/equipe/web, o mobile em github.com/equipe/mobile.
  • Monorepo: todos os componentes vivem no mesmo repositório, em diretórios separados.

A abordagem multi-repo parece mais organizada à primeira vista, mas na prática ela cria problemas de coordenação. Quando uma mudança no backend exige uma mudança correspondente no frontend (por exemplo, renomear um campo da API), você precisa abrir dois pull requests em repositórios diferentes, e garantir que os dois sejam mesclados juntos. Versões ficam dessincronizadas. Configurações são duplicadas.

A abordagem monorepo simplifica isso: uma única alteração que corta o backend e o frontend fica em um único commit, um único pull request, uma única revisão. A equipe inteira vê o projeto como um todo, não como peças separadas.

A estrutura típica de um monorepo para uma aplicação com backend, web e mobile é:

meu-projeto/
├── backend/
│   ├── src/
│   ├── package.json
│   └── tsconfig.json
├── web/
│   ├── src/
│   ├── package.json
│   └── tsconfig.json
├── mobile/
│   ├── src/
│   ├── package.json
│   └── app.json
├── .gitignore
└── README.md

Cada diretório (backend/, web/, mobile/) é um componente independente, com suas próprias dependências e configuração. Mas todos vivem sob o mesmo repositório Git, compartilhando o mesmo histórico e o mesmo processo de revisão.

É essa a estrutura que o TamoJunto seguirá. Quando chegarmos ao próximo capítulo, vamos montar o repositório com esses três pontos de entrada.

Quando usar multi-repo

Monorepo não é sempre a melhor escolha. À medida que o projeto cresce e equipes diferentes ficam responsáveis por componentes diferentes, um monorepo pode se tornar lento (muitos arquivos, histórico grande) e ruidoso (notificações sobre partes do código que não te interessam). Grandes empresas como Google e Meta usam monorepos com ferramentas especializadas para lidar com essa escala. Projetos menores, como o que construiremos neste curso, se beneficiam da simplicidade do monorepo sem sofrer essas desvantagens.

A regra é simples: se a equipe é pequena e os componentes mudam juntos, comece com monorepo. Se os componentes forem verdadeiramente independentes (equipes diferentes, ciclos de deploy diferentes, nenhuma interface compartilhada), multi-repo faz mais sentido.

Conclusão, Exercícios e Referências

Conclusão

Com o Git, o cenário do projeto_final_v3_AGORA_VAI.zip deixa de existir. Cada alteração é rastreada, cada decisão fica registrada, e duas pessoas podem trabalhar no mesmo arquivo sem medo. O fluxo de branches e pull requests garante que código é revisado antes de entrar na branch principal.

A partir do próximo capítulo, vamos colocar isso em prática: criaremos o repositório do TamoJunto como um monorepo com backend, web e mobile.

Exercícios

  1. Instale o Git na sua máquina e configure seu nome e e-mail usando git config. Verifique com git config --global --list que as informações estão corretas.

  2. Crie um diretório chamado exercicio-git, inicialize um repositório Git nele (git init) e crie um arquivo notas.txt com qualquer conteúdo. Faça um commit com a mensagem “Adicionar arquivo de notas”. Verifique o histórico com git log --oneline.

  3. No mesmo repositório, crie uma branch chamada experimento. Nela, modifique o arquivo notas.txt e faça um commit. Depois, volte à main e faça uma modificação diferente na mesma linha de notas.txt. Tente fazer o merge. Resolva o conflito que surgir.

  4. Crie um repositório no GitHub (ou GitLab). Conecte seu repositório local a ele com git remote add origin <URL>. Envie seus commits com git push. Verifique no site que os arquivos aparecem.

  5. O arquivo .gitignore abaixo tem um problema. Identifique qual é e explique o risco:

    node_modules/
    dist/
    *.log
    

Referências

Para um estudo mais aprofundado sobre Git, recomendo o livro Pro Git, de Scott Chacon e Ben Straub, disponível gratuitamente em português em https://git-scm.com/book/pt-br/v2.

Conhecendo o TamoJunto

No capítulo anterior, aprendemos a usar o Git para rastrear alterações e organizar um projeto em monorepo. Mas até agora não temos um projeto de verdade — só exemplos avulsos com arquivos .html e .css. Esse capítulo muda isso.

Vamos definir o que é o TamoJunto, que problema ele resolve, quais funcionalidades ele terá, e montar a estrutura de diretórios que vai nos acompanhar pelo resto do livro. No final, teremos um repositório Git com três diretórios vazios — um para cada componente da aplicação — e um primeiro commit registrado. O código de verdade começa no próximo capítulo.

O Problema

O Problema

Quem participa da vida universitária sabe: eventos não faltam. Hackathon do departamento de computação, grupo de estudos de redes, oficina de Arduino no laboratório de eletrônica, sarau no campus. O problema é que a divulgação é caótica. Alguns eventos aparecem num story do Instagram do organizador, outros num grupo de WhatsApp com 300 mensagens por dia, outros num cartaz colado no corredor do bloco — que ninguém lê porque o corredor está em reforma.

O resultado: as pessoas ficam sabendo dos eventos por acaso, quando ficam sabendo. E os organizadores não têm como saber quantas pessoas pretendem aparecer. Planejaram coffee break para 30 e apareceram 8. Reservaram uma sala para 20 e apareceram 45.

O TamoJunto é uma plataforma de eventos comunitários que resolve esse problema. Organizadores publicam eventos com título, descrição, local, data e capacidade máxima. Participantes navegam pela lista de eventos, veem os detalhes e confirmam presença. Simples assim.

Funcionalidades do TamoJunto

Funcionalidades do TamoJunto

O TamoJunto não vai nascer pronto. A cada grupo de capítulos, adicionamos funcionalidades novas — e cada funcionalidade corta verticalmente pela aplicação inteira, passando por backend, frontend web e frontend mobile.

Para o final do curso, o TamoJunto terá:

  • Lista de eventos — exibir todos os eventos disponíveis, com título, data, local e capacidade.
  • Detalhe do evento — visualizar todas as informações de um evento específico.
  • Criação e edição de eventos — formulário para criar e modificar eventos.
  • Confirmação de presença (RSVP) — confirmar participação num evento, com controle de capacidade.
  • Cadastro e login — registro de usuários com autenticação por e-mail e senha.
  • Papéis — organizadores podem criar e editar eventos; participantes podem confirmar presença.
  • Busca e filtros — encontrar eventos por título, data ou local.
  • Upload de imagem — capas para os eventos.
  • Notificações — lembretes de eventos próximos.
  • Contagem em tempo real — número de confirmados atualizado ao vivo.

Nos próximos capítulos, começamos pelo mínimo: uma tabela no banco de dados, um endpoint que retorna os eventos como JSON, e uma tela que os exibe. Tudo o mais aparece quando a aplicação precisar.

A Stack Tecnológica

A Stack Tecnológica

Antes de montar a estrutura do projeto, vale deixar explícito o que vamos usar e por quê.

Backend: TypeScript com Express e PostgreSQL. O TypeScript traz tipagem estática sobre o JavaScript — erros de tipo aparecem no editor, não em produção. O Express é um framework HTTP minimalista: poucas abstrações, comportamento previsível. E o PostgreSQL é um banco de dados relacional maduro, com suporte robusto a constraints, transações e tipos de dados.

Frontend Web: React com TypeScript. O React é a biblioteca de componentes mais usada no mercado. A ideia central — descrever o que a interface deve ser para um dado estado, e deixar o framework descobrir como atualizar o DOM — é o padrão que todas as alternativas modernas seguem (Vue, Angular, Svelte). Aprender React é aprender o modelo mental que se aplica a qualquer uma delas.

Frontend Mobile: React Native com Expo. O React Native permite construir aplicativos nativos para Android e iOS usando os mesmos conceitos de componentes do React. O Expo simplifica o setup — sem precisar instalar Android Studio ou Xcode para os primeiros passos. E como web e mobile compartilham TypeScript, parte da lógica (validação, formatação, chamadas de API) pode ser reaproveitada.

Essa stack não é a única possível. Existem alternativas excelentes para cada camada. A escolha reflete um equilíbrio entre adoção no mercado, qualidade da documentação e coesão entre as partes — web e mobile falando a mesma linguagem reduz a carga cognitiva.

Montando o Monorepo

Montando o Monorepo

No capítulo de ambiente de desenvolvimento, vimos que um monorepo agrupa todos os componentes da aplicação num único repositório Git. Vamos criar essa estrutura para o TamoJunto.

Abra o terminal e crie o diretório do projeto:

$ mkdir tamojunto
$ cd tamojunto
$ git init
Initialized empty Git repository in /home/maria/tamojunto/.git/

Agora, crie os três diretórios dos componentes:

$ mkdir backend web mobile

Cada diretório vai abrigar um componente independente, com suas próprias dependências e configuração:

  • backend/ — o servidor HTTP e a conexão com o banco de dados.
  • web/ — a interface web que roda no navegador.
  • mobile/ — o aplicativo que roda em dispositivos Android e iOS.

Por enquanto, os diretórios estão vazios. O Git não rastreia diretórios vazios — ele rastreia arquivos. Para que os diretórios apareçam no repositório, uma convenção comum é colocar um arquivo .gitkeep em cada um:

$ touch backend/.gitkeep web/.gitkeep mobile/.gitkeep

O .gitkeep não tem significado especial para o Git. É apenas um arquivo vazio cujo nome, por convenção, sinaliza “este diretório precisa existir no repositório”. Quando adicionarmos código de verdade em cada diretório, o .gitkeep pode ser removido.

O .gitignore do TamoJunto

Antes do primeiro commit, vamos configurar o .gitignore. Os três componentes usam Node.js, então compartilham alguns padrões:

Arquivo: .gitignore
node_modules/
dist/
.env
*.log
.DS_Store

Código 4-1: Arquivo de ignores na raiz do monorepo.

  • node_modules/ — dependências instaladas pelo npm. Cada componente terá a sua pasta node_modules/, e nenhuma delas deve ir para o repositório (são grandes e reproduzíveis via npm install).
  • dist/ — arquivos gerados pelo build (JavaScript compilado, bundles).
  • .env — variáveis de ambiente com credenciais locais.
  • *.log — logs gerados por ferramentas de desenvolvimento.
  • .DS_Store — metadados do macOS que não interessam ao projeto.

Primeiro commit

Com a estrutura pronta, vamos registrar tudo:

$ git add .
$ git status
On branch main

No commits yet

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)
	new file:   .gitignore
	new file:   backend/.gitkeep
	new file:   mobile/.gitkeep
	new file:   web/.gitkeep

Quatro arquivos. Sem surpresas — o git status antes do commit é um hábito que evita incluir algo por acidente. Agora o commit:

$ git commit -m "Criar estrutura do monorepo com backend, web e mobile"
[main (root-commit) c4a1e8f] Criar estrutura do monorepo com backend, web e mobile
 4 files changed, 5 insertions(+)
 create mode 100644 .gitignore
 create mode 100644 backend/.gitkeep
 create mode 100644 mobile/.gitkeep
 create mode 100644 web/.gitkeep

A estrutura do repositório:

tamojunto/
├── backend/
│   └── .gitkeep
├── web/
│   └── .gitkeep
├── mobile/
│   └── .gitkeep
└── .gitignore

Não parece muito. E realmente não é — essa é a ideia. O repositório começa com o mínimo necessário para que todos os componentes tenham seu lugar. O código entra incrementalmente, a cada capítulo, começando pelo backend no próximo.

Exercícios

Exercícios

  1. Crie o repositório do seu projeto seguindo a mesma estrutura de monorepo. Use git init, crie os diretórios backend/, web/ e mobile/ com .gitkeep, configure o .gitignore e faça o primeiro commit.

  2. Adicione um arquivo README.md na raiz com o nome do seu projeto, uma frase descrevendo o que ele faz e a stack tecnológica que pretende usar. Faça um commit separado.

  3. Publique o repositório no GitHub: crie um repositório vazio, adicione o remote com git remote add origin <URL> e envie seus commits com git push -u origin main. Verifique no GitHub que os diretórios e o .gitignore aparecem.

  4. O TamoJunto resolve o problema de divulgação e confirmação de presença em eventos. Descreva, em um parágrafo, qual problema o seu projeto resolve. Identifique pelo menos: uma lista (de itens, produtos, tarefas, etc.), uma tela de detalhe, um formulário de criação e uma funcionalidade que vá além de CRUD simples (busca, notificação, contagem em tempo real, etc.).

Armazenando e Servindo Dados

Até agora, temos um repositório configurado e uma noção de como frontend e backend se comunicam via HTTP. Mas a aplicação ainda não faz nada — não tem banco de dados, não tem endpoint, não tem dado nenhum. Esse capítulo muda isso.

Vamos criar as tabelas do TamoJunto no banco de dados, popular elas com dados de teste e expor esses dados através de uma API HTTP. No final, teremos um endpoint GET /api/events que retorna uma lista de eventos em formato JSON — o primeiro dado real trafegando entre o backend e qualquer cliente que queira consumi-lo.

Modelagem Relacional

Modelagem Relacional

O TamoJunto é uma plataforma de eventos comunitários. Pessoas criam eventos, outras pessoas confirmam presença. Para armazenar essas informações, vamos usar um banco de dados relacional — especificamente, o PostgreSQL.

Um banco de dados relacional organiza informações em tabelas. Cada tabela representa uma entidade do domínio (um evento, um usuário, uma confirmação de presença). Cada linha da tabela é um registro (um evento específico, um usuário específico). E cada coluna é um atributo daquele registro.

Que tabelas o TamoJunto precisa agora? Duas:

  • events — os eventos da plataforma.
  • users — as pessoas que criam e participam dos eventos.

A tabela events

Um evento no TamoJunto tem título, descrição, local, data, e uma capacidade máxima de participantes. Cada evento é criado por alguém — um organizador. E precisamos de um identificador único para cada registro.

ColunaTipoDescrição
idSERIALIdentificador único, gerado automaticamente
titleVARCHAR(200)Título do evento
descriptionTEXTDescrição detalhada
locationVARCHAR(200)Local onde o evento acontece
dateTIMESTAMPData e horário do evento
capacityINTEGERNúmero máximo de participantes
organizer_idINTEGERQuem criou o evento (referência à tabela users)
created_atTIMESTAMPQuando o registro foi inserido no banco

A coluna organizer_id é especial: ela não armazena o nome do organizador, mas sim o id de um registro na tabela users. Isso é um foreign key (chave estrangeira), e é a forma como bancos relacionais representam relações entre tabelas. Mas calma — esse foreign key ainda não existe na primeira versão da tabela. Ele será adicionado por uma segunda migration, depois que a tabela users for criada.

A tabela users

Por enquanto, um usuário tem apenas nome e e-mail:

ColunaTipoDescrição
idSERIALIdentificador único
nameVARCHAR(100)Nome do usuário
emailVARCHAR(200)E-mail (único por usuário)
created_atTIMESTAMPQuando o registro foi criado

A coluna email tem uma restrição de unicidade: dois usuários não podem ter o mesmo e-mail. Isso é um constraint (restrição) — o banco de dados impede a operação se alguém tentar violar essa regra.

Relacionamentos

A relação entre events e users é muitos para um (many-to-one): um usuário pode organizar vários eventos, mas cada evento tem exatamente um organizador. Essa relação é materializada pelo foreign key organizer_id na tabela events, que aponta para id na tabela users.

Se você tentar inserir um evento com organizer_id = 99 e não existir nenhum usuário com id = 99, o banco de dados rejeita a operação com um erro. Isso mantém a integridade dos dados — todo evento sempre tem um organizador válido.

Migrations

Migrations

Com o modelo definido, precisamos criar essas tabelas no banco de dados. Poderíamos abrir um terminal, conectar no PostgreSQL e executar os comandos CREATE TABLE manualmente. Funciona, uma vez. Mas quando um colega clonar o repositório e precisar criar o mesmo banco, ele precisaria repetir tudo.

Para resolver isso, usamos migrations (migrações) — arquivos SQL versionados que descrevem mudanças na estrutura do banco de dados. Cada migration é um passo: criar uma tabela, adicionar uma coluna, remover um constraint. A sequência completa das migrations reproduz o banco inteiro do zero.

A primeira migration: events

A primeira tabela que vamos criar é events — o recurso central do TamoJunto. Por enquanto, sem a coluna organizer_id: a tabela users ainda não existe, e o foreign key precisa referenciar uma tabela que já esteja criada.

Crie um diretório backend/migrations/ e dentro dele o arquivo 001_create_events.sql:

Arquivo: backend/migrations/001_create_events.sql
CREATE TABLE events (
    id          SERIAL PRIMARY KEY,
    title       VARCHAR(200)  NOT NULL,
    description TEXT,
    location    VARCHAR(200)  NOT NULL,
    date        TIMESTAMP     NOT NULL,
    capacity    INTEGER       NOT NULL CHECK (capacity > 0),
    created_at  TIMESTAMP     NOT NULL DEFAULT NOW()
);

Código 5-1: Migration que cria a tabela events (sem foreign key por enquanto).

Algumas decisões nesse SQL:

  • SERIAL PRIMARY KEY gera um inteiro auto-incrementado e o marca como chave primária — cada registro recebe um id único automaticamente.
  • NOT NULL impede que a coluna fique vazia. Um evento sem título ou sem local não faz sentido no TamoJunto.
  • CHECK (capacity > 0) é um constraint que impede eventos com capacidade zero ou negativa. O banco de dados valida isso a cada inserção e atualização.
  • DEFAULT NOW() preenche created_at automaticamente com a data/hora atual no momento da inserção.

A segunda migration: users e o foreign key

A segunda migration faz duas coisas: cria a tabela users e adiciona a coluna organizer_id à tabela events já existente.

Arquivo: backend/migrations/002_create_users_add_fk.sql
CREATE TABLE users (
    id         SERIAL PRIMARY KEY,
    name       VARCHAR(100)  NOT NULL,
    email      VARCHAR(200)  NOT NULL UNIQUE,
    created_at TIMESTAMP     NOT NULL DEFAULT NOW()
);

ALTER TABLE events
    ADD COLUMN organizer_id INTEGER NOT NULL DEFAULT 1 REFERENCES users(id);

Código 5-2: Migration que cria users e adiciona o foreign key em events.

O ALTER TABLE mostra algo importante: migrations não servem apenas para criar tabelas. Elas também modificam tabelas existentes — adicionar colunas, remover constraints, alterar tipos. Essa é a natureza incremental das migrations: cada arquivo descreve uma diferença em relação ao estado anterior do banco.

O DEFAULT 1 na coluna organizer_id existe porque a tabela events pode já conter registros (se o seed tivesse rodado antes, por exemplo). Sem um valor padrão, o ALTER TABLE ADD COLUMN ... NOT NULL falharia — o banco não saberia qual valor colocar nas linhas existentes. Numa aplicação real, esse valor padrão seria removido numa migration posterior; aqui, ele serve para que o SQL execute sem erros.

A cláusula REFERENCES users(id) declara o foreign key. O PostgreSQL garante que todo valor em organizer_id corresponde a um id existente na tabela users.

Executando as migrations

Para executar as migrations, vamos usar um script simples que lê os arquivos .sql do diretório migrations/ em ordem e executa cada um no banco. Crie o arquivo backend/migrate.ts:

Arquivo: backend/migrate.ts
import { Client } from "pg";
import fs from "fs";
import path from "path";

async function migrate() {
  const client = new Client({
    connectionString: process.env.DATABASE_URL,
  });
  await client.connect();

  const migrationsDir = path.join(__dirname, "migrations");
  const files = fs.readdirSync(migrationsDir).sort();

  for (const file of files) {
    if (!file.endsWith(".sql")) continue;
    const sql = fs.readFileSync(path.join(migrationsDir, file), "utf8");
    console.log(`Executando: ${file}`);
    await client.query(sql);
  }

  console.log("Migrations concluídas.");
  await client.end();
}

migrate().catch((err) => {
  console.error("Erro ao executar migrations:", err.message);
  process.exit(1);
});

Código 5-3: Script que aplica migrations em ordem.

Esse script é propositalmente simples: lê os arquivos em ordem alfabética (por isso o prefixo numérico 001_, 002_), executa cada SQL e pronto. Não rastreia quais migrations já foram aplicadas — se rodar duas vezes, a segunda execução falha porque as tabelas já existem. Para um projeto em sala de aula, isso é aceitável. Recrie o banco quando precisar rodar as migrations novamente1.

Para executar, usamos o tsx, um executor de TypeScript que compila e roda num passo só:

$ cd backend
$ DATABASE_URL=postgres://localhost:5432/tamojunto npx tsx migrate.ts
Executando: 001_create_events.sql
Executando: 002_create_users_add_fk.sql
Migrations concluídas.

As tabelas agora existem no banco. Verificando:

$ psql postgres://localhost:5432/tamojunto -c '\dt'
          List of relations
 Schema |  Name  | Type  |  Owner
--------+--------+-------+---------
 public | events | table | pedrov
 public | users  | table | pedrov
(2 rows)

  1. Em projetos de produção, ferramentas como Knex, Prisma ou Flyway rastreiam quais migrations já foram aplicadas e executam apenas as pendentes. O conceito é o mesmo, só a automação é maior. ↩

SQL: INSERT, SELECT e JOIN

SQL: INSERT, SELECT e JOIN

Com as tabelas criadas, podemos manipular dados. SQL tem quatro operações fundamentais — INSERT, SELECT, UPDATE e DELETE — mas nesse capítulo usamos apenas INSERT e SELECT. Escrita e modificação ficam para quando o TamoJunto precisar delas.

Inserindo dados

O comando INSERT adiciona registros a uma tabela. Como events agora tem um foreign key que referencia users, precisamos inserir os usuários primeiro:

INSERT INTO users (name, email) VALUES
    ('Ana Silva',    'ana@email.com'),
    ('Bruno Costa',  'bruno@email.com'),
    ('Carla Souza',  'carla@email.com');

A coluna id não aparece no INSERT — o SERIAL gera o valor automaticamente. O mesmo vale para created_at, que usa DEFAULT NOW().

Com os usuários criados, podemos inserir eventos. O organizer_id precisa ser o id de um usuário existente — aqui, usamos 1 (Ana), 2 (Bruno) e 3 (Carla):

INSERT INTO events (title, description, location, date, capacity, organizer_id)
VALUES
    ('Hackathon UFS',
     'Maratona de programação de 24h',
     'Centro de Vivência - UFS',
     '2026-09-15 08:00:00',
     50,
     1),
    ('Meetup de TypeScript',
     'Palestra sobre tipos avançados em TS',
     'Auditório do DCOMP',
     '2026-09-20 19:00:00',
     30,
     2);

E se tentarmos inserir um evento com organizer_id = 99?

$ psql postgres://localhost:5432/tamojunto -c \
    "INSERT INTO events (title, location, date, capacity, organizer_id) \
     VALUES ('Evento Fantasma', 'Lugar Nenhum', '2026-12-01', 10, 99);"
ERROR:  insert or update on table "events" violates foreign key constraint
        "events_organizer_id_fkey"
DETAIL:  Key (organizer_id)=(99) is not present in table "users".

O banco rejeitou a operação. O foreign key está fazendo seu trabalho.

Consultando dados

O comando SELECT recupera dados de uma tabela:

SELECT id, title, date, capacity FROM events;
 id |        title         |        date         | capacity
----+----------------------+---------------------+----------
  1 | Hackathon UFS        | 2026-09-15 08:00:00 |       50
  2 | Meetup de TypeScript | 2026-09-20 19:00:00 |       30
(2 rows)

O SELECT * retorna todas as colunas, mas listar as colunas explicitamente é melhor: deixa claro quais dados o código espera e evita surpresas quando alguém adiciona uma coluna nova à tabela.

Filtrar registros usa a cláusula WHERE:

SELECT title, capacity FROM events WHERE capacity >= 40;
     title     | capacity
---------------+----------
 Hackathon UFS |       50
(1 row)

JOIN: cruzando tabelas

O SELECT acima mostra organizer_id = 1, mas isso não diz muita coisa. Para obter o nome do organizador junto com os dados do evento, precisamos cruzar as tabelas events e users. Isso é um JOIN:

SELECT
    e.title,
    e.date,
    e.capacity,
    u.name AS organizer_name
FROM events e
JOIN users u ON e.organizer_id = u.id;
        title         |        date         | capacity | organizer_name
----------------------+---------------------+----------+----------------
 Hackathon UFS        | 2026-09-15 08:00:00 |       50 | Ana Silva
 Meetup de TypeScript | 2026-09-20 19:00:00 |       30 | Bruno Costa
(2 rows)

A cláusula JOIN users u ON e.organizer_id = u.id diz ao banco: “para cada evento, encontre o usuário cujo id é igual ao organizer_id do evento e junte os dados numa única linha”. Os aliases e e u são atalhos para evitar escrever o nome completo da tabela em cada coluna.

Seed Data

Seed Data

Os INSERTs que fizemos manualmente servem para aprender SQL, mas não são práticos no dia a dia. Quando um colega clona o repositório e roda as migrations, o banco está vazio — sem dados para testar, sem nada para renderizar no frontend. Para resolver isso, usamos seed data: um script que popula o banco com dados de teste.

Crie o arquivo backend/seed.sql:

Arquivo: backend/seed.sql
-- Limpar dados existentes (para poder rodar o seed repetidamente)
TRUNCATE events, users RESTART IDENTITY CASCADE;

-- Usuários
INSERT INTO users (name, email) VALUES
    ('Ana Silva',    'ana@email.com'),
    ('Bruno Costa',  'bruno@email.com'),
    ('Carla Souza',  'carla@email.com');

-- Eventos organizados pela Ana (id = 1)
INSERT INTO events (title, description, location, date, capacity, organizer_id) VALUES
    ('Hackathon UFS',
     'Maratona de programação de 24h aberta a todos os cursos',
     'Centro de Vivência - UFS', '2026-09-15 08:00', 50, 1),
    ('Workshop de Git',
     'Aprenda Git do zero com exercícios práticos',
     'Lab 1 - DCOMP', '2026-09-18 14:00', 25, 1),
    ('Café com Código',
     'Bate-papo informal sobre carreira em tech',
     'Cantina Central', '2026-09-22 10:00', 15, 1),
    ('Palestra: IA Generativa',
     'Introdução a LLMs e suas aplicações',
     'Auditório do CCET', '2026-10-01 19:00', 80, 1);

-- Eventos organizados pelo Bruno (id = 2)
INSERT INTO events (title, description, location, date, capacity, organizer_id) VALUES
    ('Meetup de TypeScript',
     'Palestra sobre tipos avançados em TS',
     'Auditório do DCOMP', '2026-09-20 19:00', 30, 2),
    ('Torneio de Ping-Pong',
     'Torneio aberto para a comunidade universitária',
     'Ginásio - UFS', '2026-09-25 15:00', 32, 2),
    ('Cineclube: Wargames',
     'Sessão comentada do filme Wargames (1983)',
     'Sala de Projeção - Biblioteca', '2026-10-05 18:30', 40, 2),
    ('Aulão de SQL',
     'Revisão intensiva de SQL para a prova',
     'Lab 2 - DCOMP', '2026-10-10 08:00', 35, 2);

-- Eventos organizados pela Carla (id = 3)
INSERT INTO events (title, description, location, date, capacity, organizer_id) VALUES
    ('Grupo de Estudos: Redes',
     'Estudo colaborativo dos capítulos 3 e 4 do Kurose',
     'Sala de Estudos - Biblioteca', '2026-09-19 16:00', 12, 3),
    ('Oficina de Arduino',
     'Monte seu primeiro circuito com Arduino Uno',
     'Lab de Eletrônica - DEL', '2026-09-28 09:00', 20, 3),
    ('Sarau Nerd',
     'Poesia, música e stand-up com tema de computação',
     'Praça da Reitoria', '2026-10-08 19:00', 60, 3),
    ('Corrida da Integração',
     'Corrida de 5km entre os cursos de exatas',
     'Pista de Atletismo - UFS', '2026-10-12 06:30', 100, 3);

Código 5-4: Seed data com 3 usuários e 12 eventos.

O TRUNCATE ... RESTART IDENTITY CASCADE no início limpa todas as tabelas e reinicia os contadores de SERIAL, permitindo rodar o seed várias vezes sem acumular duplicatas. O CASCADE é necessário porque events tem um foreign key apontando para users — sem ele, o PostgreSQL se recusaria a truncar users enquanto existirem eventos referenciando registros dela.

Para executar:

$ psql postgres://localhost:5432/tamojunto -f backend/seed.sql
TRUNCATE TABLE
INSERT 0 3
INSERT 0 4
INSERT 0 4
INSERT 0 4

12 eventos, 3 usuários. Dados suficientes para o frontend ter algo para renderizar sem precisar de paginação — e concretos o bastante para parecerem reais durante o desenvolvimento.

A Arquitetura em Camadas

A Arquitetura em Camadas

Hora de construir o backend que vai servir esses dados via HTTP. A tentação é colocar tudo num único arquivo — receber a requisição, consultar o banco, montar a resposta. Para 12 eventos e um endpoint, até funciona. Mas pense no que vem pela frente: autenticação, validação, tratamento de erro, múltiplos endpoints. Um arquivo monolítico cresce muito rápido.

A abordagem que vamos seguir é dividir o backend em camadas, cada uma com uma responsabilidade:

Route — Define as URLs e os métodos HTTP que a aplicação aceita. GET /api/events deve chamar tal função. Só mapeamento.

Controller — Recebe a requisição HTTP, extrai os dados relevantes (query params, body, headers) e repassa para a camada de serviço. Depois, formata a resposta HTTP com o status code e o body adequados. O controller conhece HTTP; as camadas abaixo dele, não.

Service — Contém a lógica de negócio. Hoje a resposta é simples — “retorna todos os eventos” — mas essa é a camada que vai crescer quando o TamoJunto ganhar regras mais complexas.

Repository — Faz as consultas SQL no banco de dados. É a única camada que sabe que o dado mora no PostgreSQL. Se um dia migrarmos para outro banco, só o repository muda.

A requisição flui assim: Route → Controller → Service → Repository → banco de dados. A resposta faz o caminho inverso.

Para um endpoint que faz SELECT * FROM events, a lógica de cada camada cabe em duas linhas. A separação não é sobre o tamanho do código hoje — é sobre onde cada tipo de decisão fica quando o projeto crescer.

Estrutura de arquivos

Dentro de backend/src/, crie os seguintes arquivos:

backend/
├── migrations/
│   ├── 001_create_events.sql
│   └── 002_create_users_add_fk.sql
├── seed.sql
├── migrate.ts
├── package.json
├── tsconfig.json
├── .env
└── src/
    ├── app.ts
    ├── config.ts
    ├── db.ts
    ├── events/
    │   ├── events.routes.ts
    │   ├── events.controller.ts
    │   ├── events.service.ts
    │   └── events.repository.ts
    └── errors/
        └── errorHandler.ts

Os arquivos de eventos estão agrupados no diretório events/. Quando o TamoJunto ganhar autenticação, teremos um diretório auth/ com a mesma estrutura. Cada feature num lugar.

Por que um framework?

O Node.js já sabe atender HTTP sozinho. O módulo node:http que vem embutido liga um servidor e entrega cada requisição para uma função. Um endpoint como o nosso ficaria assim:

import http from "node:http";

const server = http.createServer((req, res) => {
  if (req.method === "GET" && req.url === "/api/events") {
    const events = buscarEventos();
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify(events));
    return;
  }
  res.writeHead(404, { "Content-Type": "application/json" });
  res.end(JSON.stringify({ error: "Not Found" }));
});

server.listen(3000);

Código 5-5: O mesmo endpoint escrito só com o módulo node:http.

Funciona. O problema aparece no segundo endpoint, e no terceiro. Cada rota vira mais um if comparando req.method e req.url na mão. Ler o corpo de um POST significa juntar pedaços do req conforme eles chegam pela rede. Um parâmetro como /api/events/42 exige fatiar a string da URL você mesmo. Nada disso é lógica do TamoJunto: é a mesma canalização que todo servidor HTTP reescreve.

Um framework é uma biblioteca que já resolve essa canalização e te dá uma estrutura pronta para pendurar o seu código. Você não chama o framework quando quer; ele chama o seu código quando uma requisição chega. O roteamento, a leitura do corpo, a extração de parâmetros da URL, o framework cuida disso, e você escreve só a parte que é do seu problema.

O Express é um framework HTTP minimalista para Node.js. O endpoint acima, com Express, vira uma linha de roteamento e uma função que responde, sem nenhum if de despacho. É o que vamos construir a seguir.

Instalando as dependências

Antes de escrever código, inicialize o projeto e instale os pacotes necessários:

$ cd backend
$ npm init -y
$ npm install express pg dotenv
$ npm install -D typescript @types/express @types/pg @types/node tsx

Além do express, o pg é o cliente PostgreSQL e o dotenv carrega variáveis de ambiente de um arquivo .env. Os pacotes com @types/ são as definições de tipo para TypeScript, e tsx é o executor que compila e roda TypeScript num passo só.

Configuração e Variáveis de Ambiente

Configuração e Variáveis de Ambiente

De onde o backend lê a URL do banco de dados e a porta HTTP? Colocar essas informações diretamente no código é ruim por dois motivos: qualquer mudança exige editar e redeployar o código, e credenciais ficam expostas no repositório.

A solução padrão é usar variáveis de ambiente. O código lê os valores de process.env, e cada máquina (o laptop do desenvolvedor, o servidor de produção) define seus próprios valores.

Crie o arquivo backend/src/config.ts:

Arquivo: backend/src/config.ts
export interface AppConfig {
  port: number;
  databaseUrl: string;
}

export function loadConfig(): AppConfig {
  const databaseUrl = process.env.DATABASE_URL;

  if (!databaseUrl) {
    console.error("Erro: DATABASE_URL não está definida.");
    process.exit(1);
  }

  return {
    port: parseInt(process.env.PORT ?? "3000", 10),
    databaseUrl,
  };
}

Código 5-6: Carregamento e validação da configuração.

A validação acontece no momento em que o servidor inicia. Se DATABASE_URL não estiver definida, o processo para imediatamente com uma mensagem clara. Descobrir que faltou uma variável de ambiente só quando a primeira requisição chega e o banco recusa a conexão é uma experiência bem pior.

A porta HTTP tem um valor padrão (3000), mas DATABASE_URL não tem — uma URL de banco errada é pior do que nenhuma URL, porque o servidor conectaria num banco que não é o certo.

Para facilitar o desenvolvimento, crie um arquivo .env na raiz do backend/:

Arquivo: backend/.env
DATABASE_URL=postgres://localhost:5432/tamojunto
PORT=3000

Código 5-7: Variáveis de ambiente para desenvolvimento local.

Essa URL não tem senha, e isso costuma gerar dúvida. A forma completa de uma URL de conexão do PostgreSQL é postgres://usuario:senha@host:porta/banco. Na sua máquina, o PostgreSQL vem configurado para confiar em conexões locais do seu próprio usuário do sistema operacional, então usuario, senha e @ podem ser omitidos: conectar em localhost já basta como prova de identidade. É por isso que o psql postgres://localhost:5432/tamojunto das migrations funcionou sem pedir nada.

Em produção o cenário é outro. O banco roda numa máquina separada, exposto na rede, e aceitar qualquer conexão sem autenticação seria um convite. Lá a DATABASE_URL carrega usuário e senha reais, algo como postgres://tamojunto_app:kR7$m02...@db.internal:5432/tamojunto. Essa é exatamente a credencial que não pode vazar.

Adicione .env ao .gitignore. O arquivo contém configuração específica de cada máquina e, em produção, a senha do banco. Cada desenvolvedor tem o seu, e nenhum deles vai parar no repositório.

Para que o Node.js carregue o .env automaticamente, chamamos import "dotenv/config" no ponto de entrada da aplicação. Veremos isso no app.ts a seguir.

O Primeiro GET Endpoint

O Primeiro GET Endpoint

Com a configuração resolvida, vamos implementar as camadas de baixo para cima: banco de dados, repository, service, controller e route.

Conexão com o banco

Arquivo: backend/src/db.ts
import { Pool } from "pg";
import { loadConfig } from "./config";

const config = loadConfig();
export const pool = new Pool({ connectionString: config.databaseUrl });

Código 5-8: Pool de conexões com o PostgreSQL.

Usamos Pool em vez de Client aqui. Um Client abre uma única conexão com o banco — se duas requisições chegarem ao mesmo tempo, uma precisa esperar a outra terminar. O Pool mantém várias conexões abertas e distribui entre as requisições. Para o migrate.ts, que roda uma vez e sai, Client é adequado. Para um servidor que atende requisições continuamente, Pool.

Repository

Arquivo: backend/src/events/events.repository.ts
import { pool } from "../db";

export async function findAll() {
  const result = await pool.query(`
    SELECT
      e.id,
      e.title,
      e.description,
      e.location,
      e.date,
      e.capacity,
      e.created_at,
      u.name AS organizer_name
    FROM events e
    JOIN users u ON e.organizer_id = u.id
    ORDER BY e.date ASC
  `);
  return result.rows;
}

Código 5-9: Repository que busca todos os eventos no banco.

A query já faz o JOIN com users para trazer o nome do organizador — o frontend precisa exibir essa informação, e fazer duas consultas separadas seria desperdício. O ORDER BY e.date ASC retorna os eventos em ordem cronológica.

Service

Arquivo: backend/src/events/events.service.ts
import * as eventsRepository from "./events.repository";

export async function listAll() {
  return eventsRepository.findAll();
}

Código 5-10: Service que (por enquanto) apenas delega ao repository.

Nesse momento, o service parece inútil — ele apenas repassa a chamada para o repository. A camada existe porque é aqui que lógica de negócio vai morar quando aparecer. Filtrar eventos por data, verificar permissões de acesso — tudo isso entra no service sem mexer no repository nem no controller.

Controller

Arquivo: backend/src/events/events.controller.ts
import { Request, Response, NextFunction } from "express";
import * as eventsService from "./events.service";

export async function getAll(req: Request, res: Response, next: NextFunction) {
  try {
    const events = await eventsService.listAll();
    res.json(events);
  } catch (err) {
    next(err);
  }
}

Código 5-11: Controller que responde com a lista de eventos em JSON.

O controller chama o service, formata a resposta (o res.json serializa o array para JSON e define o header Content-Type: application/json automaticamente) e, se der erro, repassa para o middleware de tratamento de erros via next(err).

Route

Arquivo: backend/src/events/events.routes.ts
import { Router } from "express";
import * as eventsController from "./events.controller";

const router = Router();
router.get("/api/events", eventsController.getAll);

export default router;

Código 5-12: Rota que mapeia GET /api/events ao controller.

A rota é a camada mais fina: apenas conecta uma URL e um método HTTP a uma função do controller.

Montando tudo no app.ts

Arquivo: backend/src/app.ts
import "dotenv/config";
import express from "express";
import eventsRoutes from "./events/events.routes";
import { errorHandler } from "./errors/errorHandler";
import { loadConfig } from "./config";

const config = loadConfig();
const app = express();

app.use(eventsRoutes);
app.use(errorHandler);

app.listen(config.port, () => {
  console.log(`Servidor rodando em http://localhost:${config.port}`);
});

Código 5-13: Ponto de entrada do servidor.

O import "dotenv/config" na primeira linha carrega as variáveis do arquivo .env para process.env. Precisa estar antes de qualquer código que leia variáveis de ambiente — inclusive antes do loadConfig(). O dotenv procura o arquivo .env no diretório de onde o processo foi executado, por isso todos os comandos a seguir usam cd backend antes de rodar o servidor.

Testando o endpoint

Inicie o servidor e faça uma requisição com curl:

$ cd backend
$ npx tsx src/app.ts
Servidor rodando em http://localhost:3000

Em outro terminal:

$ curl -s http://localhost:3000/api/events | jq '.[0]'
{
  "id": 1,
  "title": "Hackathon UFS",
  "description": "Maratona de programação de 24h aberta a todos os cursos",
  "location": "Centro de Vivência - UFS",
  "date": "2026-09-15T11:00:00.000Z",
  "capacity": 50,
  "created_at": "2026-08-21T14:32:07.412Z",
  "organizer_name": "Ana Silva"
}

O primeiro evento do TamoJunto, servido como JSON. O frontend vai consumir exatamente essa resposta no próximo capítulo.

Para conferir que todos os 12 eventos estão lá:

$ curl -s http://localhost:3000/api/events | jq 'length'
12

Respostas de Erro Estruturadas

Respostas de Erro Estruturadas

Sem tratamento de erros, o Express retorna HTML genérico quando algo dá errado — inútil para um cliente que espera JSON. Vamos definir um formato consistente para a API:

{
  "error": {
    "code": "ROUTE_NOT_FOUND",
    "message": "A rota solicitada não existe."
  }
}

O campo code é um identificador legível por máquina (o frontend pode usar para decidir o que exibir). O message é uma descrição legível por humano, útil para debug.

Middleware de tratamento de erros

No Express, um middleware de erro é uma função com quatro parâmetros — err, req, res, next. O Express identifica o error handler pela aridade da função:

Arquivo: backend/src/errors/errorHandler.ts
import { Request, Response, NextFunction } from "express";

interface AppError extends Error {
  statusCode?: number;
  code?: string;
}

export function errorHandler(
  err: AppError,
  req: Request,
  res: Response,
  next: NextFunction,
) {
  console.error(err);

  const statusCode = err.statusCode ?? 500;
  const code = err.code ?? "INTERNAL_ERROR";
  const message = err.message ?? "Erro interno do servidor.";

  res.status(statusCode).json({
    error: { code, message },
  });
}

Código 5-14: Middleware que captura erros e responde com JSON estruturado.

Quando o controller chama next(err), o Express pula todos os middlewares normais e vai direto para esse handler. O erro é logado no console do servidor (para o desenvolvedor) e uma resposta JSON é enviada ao cliente.

Rota inexistente

Uma requisição para uma rota que não existe precisa retornar 404 com o nosso formato de erro. Adicione este middleware no app.ts, depois das rotas e antes do errorHandler:

app.use(eventsRoutes);

app.use((req: Request, res: Response, next: NextFunction) => {
  const err: AppError = new Error("A rota solicitada não existe.");
  err.statusCode = 404;
  err.code = "ROUTE_NOT_FOUND";
  next(err);
});

app.use(errorHandler);

Código 5-15: Middleware de 404 posicionado entre as rotas e o error handler no app.ts.

Testando:

$ curl -s http://localhost:3000/api/naoexiste | jq
{
  "error": {
    "code": "ROUTE_NOT_FOUND",
    "message": "A rota solicitada não existe."
  }
}

E se o banco de dados estiver fora do ar? O pool.query lança um erro com code = "ECONNREFUSED", que o middleware captura:

$ curl -s http://localhost:3000/api/events | jq
{
  "error": {
    "code": "ECONNREFUSED",
    "message": "connect ECONNREFUSED 127.0.0.1:5432"
  }
}

Repare que o code aqui é o código interno do Node.js, não um código de aplicação. E a mensagem expõe o IP e a porta do banco — em produção, isso seria um problema de segurança. No ambiente de desenvolvimento, essa transparência ajuda: você sabe imediatamente que o PostgreSQL não está rodando. A distinção entre erros públicos e internos vem num capítulo posterior1.


  1. O error handler vai precisar distinguir entre erros que o cliente pode ver (como “rota não encontrada”) e erros internos (como falha de conexão com o banco). Erros internos devem retornar uma mensagem genérica ao cliente e logar os detalhes apenas no servidor. ↩

Conclusão, Exercícios e Referências

Conclusão

O TamoJunto agora tem um banco de dados com duas tabelas relacionadas, 12 eventos de teste e um endpoint HTTP que os serve como JSON. É pouco, mas é a base sobre a qual tudo mais se constrói — o frontend que vamos criar no próximo capítulo vai consumir exatamente essa resposta de GET /api/events.

Exercícios

  1. Adicione uma coluna status à tabela events (valores possíveis: scheduled, cancelled, completed). Escreva a migration SQL. Qual tipo de coluna você usaria? Pesquise sobre VARCHAR com CHECK versus o tipo ENUM no PostgreSQL e justifique sua escolha.

  2. O seed data atual insere eventos com datas futuras. Adicione 3 eventos com datas no passado e status completed. Verifique que o SELECT retorna todos os 15 eventos com ORDER BY date ASC.

  3. Escreva uma query SQL que retorne apenas os eventos organizados pela Carla, mostrando título, local e data. Use WHERE com o nome dela (via JOIN), não com o id diretamente.

  4. O script migrate.ts atual falha se você rodá-lo duas vezes. Modifique as migrations para usar CREATE TABLE IF NOT EXISTS e teste que rodar o script duas vezes funciona sem erros.

  5. Modifique o events.repository.ts para que findAll aceite um parâmetro opcional organizerId. Quando presente, a query deve filtrar por organizer_id. Teste chamando curl com um query param (ex: GET /api/events?organizerId=2) — você vai precisar modificar o controller e o service para propagar o parâmetro.

  6. Adicione um endpoint GET /api/events/:id que retorna um único evento pelo seu id. Se o evento não existir, retorne um erro 404 usando o formato { error: { code, message } }. Implemente nas quatro camadas (route, controller, service, repository).

Referências

A documentação oficial do PostgreSQL é uma referência completa para tudo que envolve SQL e configuração do banco: https://www.postgresql.org/docs/current/.

Para a biblioteca pg (node-postgres), que usamos para conectar no banco a partir do Node.js, a documentação está em https://node-postgres.com/.

A documentação do Express sobre tratamento de erros explica os detalhes do middleware de quatro parâmetros: https://expressjs.com/en/guide/error-handling.html.

Frontend Web: HTML, CSS, JavaScript e o DOM

No capítulo anterior, construímos um backend que serve 12 eventos do TamoJunto como JSON. Se você acessar http://localhost:3000/api/events no navegador, vai ver um bloco de texto denso, cheio de chaves e aspas. Funcional para um programador com jq no terminal. Inútil para um usuário comum.

Esse capítulo constrói a outra ponta: uma página web que busca esses dados e os exibe de forma que uma pessoa consiga ler, entender e interagir. Faremos tudo com HTML, CSS e JavaScript puros — sem framework, sem biblioteca, sem ferramenta de build. O objetivo é entender o que acontece por baixo, para que no próximo capítulo, quando um framework entrar em cena, você saiba exatamente qual problema ele resolve.

HTML: a Estrutura do Documento

HTML: a Estrutura do Documento

HTML (HyperText Markup Language) é a linguagem que define a estrutura de uma página web. Não é uma linguagem de programação — HTML não tem variáveis, condições ou loops. É uma linguagem de marcação: você descreve o que cada pedaço de conteúdo é (um parágrafo, um título, uma lista), e o navegador decide como exibir.

A unidade básica do HTML é o elemento. Um elemento é composto por uma tag de abertura, um conteúdo e uma tag de fechamento:

<p>Este é um parágrafo.</p>

A tag <p> diz ao navegador: “o conteúdo entre <p> e </p> é um parágrafo”. Existem dezenas de tags — <h1> a <h6> para títulos, <ul> e <li> para listas, <a> para links, <img> para imagens. Cada tag carrega significado semântico: um <button> não é um <div> estilizado. O navegador, leitores de tela e motores de busca tratam cada um de forma diferente.

Elementos podem ter atributos — informações adicionais na tag de abertura:

<a href="https://ufs.br">Site da UFS</a>
<img src="logo.png" alt="Logo do TamoJunto">

O href diz para onde o link aponta. O alt descreve a imagem para quem não pode vê-la (leitores de tela, conexões lentas). Atributos configuram o comportamento; o conteúdo entre as tags define o que aparece.

O esqueleto de uma página

Toda página HTML segue uma estrutura mínima:

Arquivo: web/index.html
<!DOCTYPE html>
<html lang="pt-BR">
<head>
  <meta charset="UTF-8">
  <title>TamoJunto</title>
</head>
<body>
  <h1>Eventos</h1>
  <p>Conteúdo vai aqui.</p>
</body>
</html>

Código 6-1: Estrutura mínima de uma página HTML.

O <head> contém metadados — título da aba, codificação de caracteres, referências a arquivos CSS. Nada do <head> aparece na página em si. O <body> é o conteúdo visível.

Um card de evento

Vamos criar um card estático para um evento do TamoJunto. Por enquanto, os dados são escritos diretamente no HTML — depois conectamos na API.

Arquivo: web/index.html
<!DOCTYPE html>
<html lang="pt-BR">
<head>
  <meta charset="UTF-8">
  <title>TamoJunto - Eventos</title>
</head>
<body>
  <h1>Eventos</h1>

  <article>
    <h2>Hackathon UFS</h2>
    <time datetime="2026-09-15T08:00">15/09/2026, 08:00</time>
    <p>Maratona de programação de 24h aberta a todos os cursos</p>
    <p>📍 Centro de Vivência - UFS</p>
    <p>Vagas: 50</p>
    <p>Organizado por Ana Silva</p>
    <button>RSVP</button>
  </article>
</body>
</html>

Código 6-2: Um card de evento com HTML semântico.

Abre esse arquivo no navegador (basta dar um duplo clique) e você verá algo feio. Texto preto sobre fundo branco, sem nenhuma separação visual entre os elementos. Tudo grudado. Mas a estrutura está correta:

  • <article> marca um bloco de conteúdo independente — faz sentido sozinho, fora do contexto da página.
  • <h2> é um título de segundo nível (o <h1> é “Eventos”, o título da página).
  • <time> associa um texto legível por humano (15/09/2026, 08:00) a um valor padronizado (datetime="2026-09-15T08:00") que máquinas entendem. Calendários e motores de busca usam esse atributo.
  • <button> é um botão nativo do navegador. Ele recebe foco pelo teclado, é ativado com Enter ou Espaço, e leitores de tela o anunciam como “botão”. Um <div> precisaria de código extra para fazer tudo isso.

A escolha dos elementos importa. O navegador já traz comportamento embutido para <button>, <a>, <form>, <input>. Usar as tags certas significa menos código e melhor acessibilidade.

CSS: a Descrição Visual

CSS: a Descrição Visual

O HTML define o que o conteúdo é. O CSS (Cascading Style Sheets) define como ele aparece. Cores, fontes, espaçamento, posicionamento — tudo isso é responsabilidade do CSS.

CSS funciona com regras. Cada regra tem um seletor (que elementos são afetados) e um bloco de declarações (que propriedades mudam):

h1 {
  color: #1a202c;
  font-size: 1.8rem;
}

Esse CSS diz: “todos os <h1> devem ter a cor #1a202c (um cinza escuro) e tamanho de fonte 1.8rem”.

Estilizando o card

Adicione um bloco <style> dentro do <head>:

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>TamoJunto - Eventos</title>
  <style>
    body {
      font-family: system-ui, sans-serif;
      background: #f7fafc;
      color: #1a202c;
      max-width: 720px;
      margin: 0 auto;
      padding: 2rem 1rem;
    }

    article {
      background: #fff;
      border: 1px solid #e2e8f0;
      border-radius: 8px;
      padding: 1.5rem;
      margin-bottom: 1rem;
    }

    article h2 {
      margin: 0 0 0.5rem;
      font-size: 1.3rem;
    }

    article time {
      display: block;
      color: #718096;
      font-size: 0.9rem;
      margin-bottom: 0.75rem;
    }

    article p {
      margin: 0.25rem 0;
      font-size: 0.95rem;
    }

    button {
      margin-top: 1rem;
      padding: 0.5rem 1.5rem;
      background: #3182ce;
      color: #fff;
      border: none;
      border-radius: 4px;
      cursor: pointer;
      font-size: 0.9rem;
    }

    button:hover {
      background: #2b6cb0;
    }
  </style>
</head>

Código 6-3: CSS que transforma o card de evento numa interface apresentável.

Recarregue a página. O card agora tem fundo branco, bordas arredondadas, espaçamento confortável e um botão azul. Mesma estrutura HTML — aparência completamente diferente.

Seletores e a cascata

No CSS acima, article h2 é um seletor descendente: seleciona todo <h2> que esteja dentro de um <article>. Se houvesse um <h2> fora de um article, essa regra não o afetaria.

E quando duas regras conflitam? Se eu escrever:

h2 { color: red; }
article h2 { color: blue; }

Qual cor o <h2> dentro do <article> terá? Azul. O CSS tem regras de especificidade: um seletor mais específico (article h2) vence um mais genérico (h2). Se a especificidade for igual, a última regra no arquivo vence — a cascata (cascade, o “C” de CSS).

Existem vários tipos de seletores: por tag (p), por classe (.card), por id (#header), por atributo ([type="text"]), combinados (article > h2). Cada tipo tem um peso diferente de especificidade. A regra completa está documentada na MDN1, mas o princípio geral é: quanto mais específico o seletor, maior a prioridade.


  1. A regra completa de especificidade está documentada em Specificity — MDN. Na prática, usar apenas seletores de classe (.card, .event-title) e evitar !important resolve 90% dos conflitos. ↩

Layout: Box Model e Flexbox

Layout: Box Model e Flexbox

Cada elemento HTML é renderizado como uma caixa (box) retangular. Essa caixa tem quatro camadas, de dentro para fora:

  1. Content — o conteúdo em si (texto, imagem).
  2. Padding — espaço interno entre o conteúdo e a borda.
  3. Border — a borda visível da caixa.
  4. Margin — espaço externo entre a borda e os elementos vizinhos.

Quando definimos padding: 1.5rem no card, estamos dizendo: “entre o conteúdo e a borda, coloque 1.5rem de espaço em todos os lados”. A propriedade margin-bottom: 1rem cria espaço entre um card e o próximo.

Organizando cards com Flexbox

Duplique o <article> três vezes no HTML para ter três cards. Eles aparecem empilhados verticalmente — é o comportamento padrão. Para organizá-los lado a lado, envolva os cards num contêiner e aplique Flexbox:

<div id="event-list">
  <article>...</article>
  <article>...</article>
  <article>...</article>
</div>
#event-list {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
}

#event-list article {
  flex: 1 1 300px;
}

Código 6-4: Flexbox para organizar os cards de evento em colunas.

display: flex transforma o contêiner numa caixa flexível. Os filhos (os cards) se alinham lado a lado automaticamente. flex-wrap: wrap permite que eles “quebrem” para a próxima linha quando não cabem. gap: 1rem define o espaço entre os cards. E flex: 1 1 300px diz que cada card tem no mínimo 300px de largura, mas pode crescer para preencher o espaço disponível.

O resultado: numa tela larga, dois ou três cards por linha; numa tela estreita, um por linha. A estrutura HTML não mudou — só o CSS controla o layout. Essa separação entre conteúdo e apresentação é um dos princípios fundamentais do desenvolvimento web.

JavaScript no Navegador: Scripts, o DOM e Eventos

JavaScript no Navegador: Scripts, o DOM e Eventos

HTML e CSS produzem páginas estáticas. Para que a página faça algo — reagir a um clique, buscar dados de um servidor, atualizar o conteúdo sem recarregar — precisamos de JavaScript.

O navegador executa JavaScript no contexto da página. O script pode acessar e modificar os elementos HTML através de uma API chamada DOM (Document Object Model). O DOM é uma representação em árvore do documento HTML: cada elemento é um nó, e os nós têm relações de pai-filho que espelham o aninhamento das tags.

Selecionando e modificando elementos

Adicione um <script> antes do </body>:

<script>
  const button = document.querySelector("button");
  console.log(button.textContent);
</script>

document.querySelector("button") busca o primeiro <button> no DOM e retorna uma referência a ele. A partir dessa referência, você pode ler atributos (button.textContent), modificar estilos (button.style.background = "red"), adicionar classes (button.classList.add("active")) — qualquer propriedade do elemento está acessível.

Respondendo a eventos

Um evento é algo que acontece na página: um clique, um movimento do mouse, uma tecla pressionada, o carregamento de um recurso. Para reagir a um evento, usamos addEventListener:

<script>
  const button = document.querySelector("button");

  button.addEventListener("click", () => {
    button.textContent = "Confirmado ✓";
    button.style.background = "#38a169";
  });
</script>

Código 6-5: Script que altera o botão RSVP ao ser clicado.

Clique no botão. O texto muda para “Confirmado ✓” e a cor fica verde. O JavaScript modificou o DOM em tempo real, sem recarregar a página.

O primeiro argumento de addEventListener é o nome do evento ("click"). O segundo é uma função callback — o código que executa quando o evento acontece. Essa função recebe um objeto event com detalhes (posição do mouse, tecla pressionada, elemento que disparou o evento), mas aqui não precisamos dele.

Buscando Dados da API

Buscando Dados da API

Os cards até agora têm dados escritos diretamente no HTML. Isso não escala: toda vez que alguém cria um evento no TamoJunto, alguém teria que editar o arquivo HTML. O que precisamos é buscar os dados da API construída no capítulo anterior.

Servindo a página pelo backend

Antes de fazer o fetch, tem um detalhe de infraestrutura. Se você abrir o arquivo HTML com duplo clique, o navegador carrega ele via protocolo file://. Nesse caso, uma chamada fetch("http://localhost:3000/api/events") vai ser bloqueada pelo navegador — uma política de segurança chamada CORS (Cross-Origin Resource Sharing) impede que uma página de uma origem (file://) faça requisições para outra (http://localhost:3000)1.

A solução mais simples: servir a página HTML pelo próprio backend. Assim, a página e a API estão na mesma origem, e o navegador permite a requisição sem restrições.

Crie um diretório web/ na raiz do projeto e mova o index.html para lá. Depois, adicione uma linha no app.ts do backend, antes das rotas da API:

import path from "path";

// ... (antes das rotas)
app.use(express.static(path.join(__dirname, "../../web")));
app.use(eventsRoutes);

Código 6-6: Servindo arquivos estáticos do diretório web/ pelo Express.

Agora acesse http://localhost:3000/ no navegador — a página carrega. E como a página está na mesma origem que a API, o fetch vai funcionar sem problemas.

Renderizando a lista com fetch

Remova os cards estáticos do HTML. Deixe apenas o contêiner vazio:

Arquivo: web/index.html
<body>
  <h1>TamoJunto — Eventos</h1>
  <div id="event-list"></div>

  <script>
    async function loadEvents() {
      const response = await fetch("/api/events");
      const events = await response.json();

      const container = document.getElementById("event-list");

      for (const ev of events) {
        const card = document.createElement("article");

        const title = document.createElement("h2");
        title.textContent = ev.title;

        const date = document.createElement("time");
        date.setAttribute("datetime", ev.date);
        date.textContent = new Date(ev.date).toLocaleString("pt-BR", {
          dateStyle: "short",
          timeStyle: "short",
        });

        const desc = document.createElement("p");
        desc.textContent = ev.description;

        const location = document.createElement("p");
        location.textContent = `📍 ${ev.location}`;

        const info = document.createElement("p");
        info.textContent =
          `Vagas: ${ev.capacity} · Organizado por ${ev.organizer_name}`;

        const btn = document.createElement("button");
        btn.textContent = "RSVP";

        card.append(title, date, desc, location, info, btn);
        container.appendChild(card);
      }
    }

    loadEvents();
  </script>
</body>

Código 6-7: Renderizando os 12 eventos a partir da API com manipulação direta do DOM.

Recarregue a página. Os 12 eventos aparecem, com dados reais vindos do banco de dados. Abra o DevTools do navegador (F12), vá na aba “Network” e recarregue: você verá a requisição GET /api/events e a resposta JSON com os 12 objetos.

O que esse código faz, passo a passo:

  1. fetch("/api/events") dispara uma requisição HTTP GET para a API. O await espera a resposta chegar.
  2. response.json() lê o corpo da resposta e parseia o JSON, retornando um array de objetos JavaScript.
  3. Para cada evento, criamos elementos HTML com document.createElement, preenchemos seus conteúdos com textContent e os montamos dentro do card com append.
  4. Cada card é adicionado ao contêiner com appendChild.

A data chega como string ISO ("2026-09-15T11:00:00.000Z") e precisa ser formatada para o leitor. new Date(ev.date).toLocaleString("pt-BR") converte para o formato brasileiro:

15/09/2026, 08:00

O que falta

Esse código não trata erros. Se o backend estiver fora do ar, a página fica em branco — nenhuma mensagem, nenhuma indicação de que algo deu errado. Também não mostra nenhum feedback enquanto os dados carregam. Esses problemas existem, mas vamos tolerá-los por agora. O próximo capítulo introduz um framework que torna esses cenários muito mais simples de lidar.


  1. CORS é o mecanismo pelo qual o navegador decide se uma página pode fazer requisições para um servidor de outra origem. Veremos CORS em detalhes quando implementarmos autenticação, onde as requisições do frontend precisam enviar cookies para o backend. ↩

O Problema da Sincronização

O Problema da Sincronização

O botão RSVP funciona de uma forma limitada: ele muda de texto quando clicado. Vamos expandir um pouco. Cada card agora mostra um contador de confirmações, e clicar no botão incrementa esse contador:

for (const ev of events) {
  // ... (criar título, data, descrição, local como antes)

  let rsvpCount = 0;

  const counter = document.createElement("p");
  counter.textContent = `Confirmações: ${rsvpCount}`;

  const btn = document.createElement("button");
  btn.textContent = "RSVP";
  btn.addEventListener("click", () => {
    rsvpCount++;
    counter.textContent = `Confirmações: ${rsvpCount}`;
  });

  card.append(title, date, desc, location, info, counter, btn);
  container.appendChild(card);
}

Código 6-8: Botão RSVP que incrementa um contador local de confirmações.

Funciona. Clique no botão, o número sobe. Mas agora imagine que a página também mostra um resumo no topo — o total de confirmações de todos os eventos:

const summary = document.createElement("p");
summary.textContent = "Total de confirmações: 0";
document.body.insertBefore(summary, container);

Quando o usuário clica num botão RSVP, o contador daquele card atualiza. Mas o resumo no topo continua mostrando 0. Para atualizá-lo, precisamos encontrar o elemento do resumo e recalcular o total:

btn.addEventListener("click", () => {
  rsvpCount++;
  counter.textContent = `Confirmações: ${rsvpCount}`;

  // Atualizar o resumo no topo
  totalCount++;
  summary.textContent = `Total de confirmações: ${totalCount}`;
});

Dois lugares exibindo o mesmo dado, duas linhas de código para atualizar. Agora adicione um terceiro: um badge no título da página mostrando o total.

btn.addEventListener("click", () => {
  rsvpCount++;
  counter.textContent = `Confirmações: ${rsvpCount}`;

  totalCount++;
  summary.textContent = `Total de confirmações: ${totalCount}`;

  // Atualizar o badge no título
  pageTitle.textContent = `TamoJunto — Eventos (${totalCount})`;
});

Três lugares, três linhas. E se esquecermos um? O dado fica inconsistente — o card mostra 3, o resumo mostra 2, o título mostra 1. O usuário vê números diferentes para a mesma informação. E cada novo lugar que exibir esse dado é mais uma linha que alguém precisa lembrar de adicionar1.

O problema não é que o código está errado. Cada linha individualmente faz o que deve. O problema é que o desenvolvedor está fazendo um trabalho que deveria ser automático: toda vez que um dado muda, todo lugar que exibe esse dado precisa ser atualizado manualmente. A manipulação direta do DOM não oferece nenhum mecanismo para isso.

Essa é a sincronização de estado — o dado (state) e a interface (view) precisam estar em sincronia, e com manipulação direta do DOM, manter essa sincronia é responsabilidade inteiramente do programador.


  1. O contador de RSVP aqui é puramente local — ele existe apenas no JavaScript da página. Nenhuma requisição é enviada ao backend, e recarregar a página zera tudo. Endpoints de escrita (como POST /api/rsvps) só aparecem quando o TamoJunto precisar persistir essas confirmações. ↩

De Onde Vêm os Componentes

De Onde Vêm os Componentes

A raiz do problema é que estamos descrevendo como atualizar a interface. A cada mudança de estado, o código precisa localizar cada elemento afetado e modificá-lo individualmente. Quanto mais lugares exibem o mesmo dado, mais código de atualização.

A alternativa é inverter a abordagem: em vez de descrever como atualizar, descrever o que a interface deve ser para um dado estado. O framework fica responsável por comparar o estado anterior com o novo e descobrir quais elementos do DOM precisam mudar.

Um componente é exatamente isso: uma função que recebe dados e retorna a descrição da interface correspondente. Quando os dados mudam, o componente é chamado novamente, produz uma nova descrição, e o framework aplica as diferenças no DOM.

Com componentes, o contador de RSVP e o resumo no topo simplesmente leem o mesmo estado. Quando o estado muda, ambos são re-renderizados automaticamente. O programador não precisa localizar elementos nem coordenar atualizações — essa é a parte que o framework automatiza.

Esse é o assunto do próximo capítulo.

Conclusão, Exercícios e Referências

Conclusão

Nesse capítulo, construímos uma página web que busca dados da API e os renderiza com HTML, CSS e JavaScript puros. Os 12 eventos do TamoJunto saíram do banco de dados, passaram pelo backend e chegaram na tela do navegador. Ao mesmo tempo, sentimos a fricção da manipulação direta do DOM: manter múltiplas partes da interface em sincronia com o mesmo dado é trabalhoso e frágil. Componentes resolvem exatamente esse problema.

Exercícios

  1. Adicione uma barra de busca (<input>) no topo da página. Quando o usuário digita, filtre os cards para mostrar apenas eventos cujo título contém o texto digitado. Use o evento "input" no <input> e a propriedade style.display para esconder/mostrar os cards.

  2. Modifique o card para exibir a data do evento formatada de duas formas: data completa (ex: “segunda-feira, 15 de setembro de 2026”) e apenas o horário (ex: “08:00”). Consulte as opções de toLocaleString na MDN.

  3. Adicione um estado “loading” à página: antes do fetch retornar, mostre o texto “Carregando eventos…” dentro do contêiner. Quando os dados chegarem, substitua pelo conteúdo real. Dica: use container.textContent para limpar e depois appendChild para popular.

  4. Trate erros no fetch: se a requisição falhar (servidor fora do ar, rede sem conexão), mostre uma mensagem de erro no lugar da lista. Use try/catch ao redor do fetch e response.ok para checar se a resposta é 2xx.

  5. Implemente o botão RSVP como um toggle: o primeiro clique marca como “Confirmado ✓” (verde), o segundo clique desmarca (volta ao estado original). Use classList.toggle para alternar uma classe CSS.

  6. Adicione ordenação à lista: dois botões no topo (“Por data” e “Por capacidade”) que reordenam os cards. Você pode reordenar o array de eventos e re-renderizar a lista do zero, ou manipular a ordem dos elementos existentes no DOM. Tente as duas abordagens e compare.

Referências

A MDN Web Docs é a referência definitiva para HTML, CSS e JavaScript no navegador. Os guias de aprendizado são um bom ponto de partida:

Frontend Web: Componentes

No capítulo anterior, construímos a lista de eventos do TamoJunto com HTML, CSS e JavaScript puros. Os 12 eventos saem do banco, passam pelo backend e chegam na tela. Funciona — mas sentimos a fricção. Cada novo lugar que exibe um dado precisa de código manual para se manter em sincronia com o restante da página.

Esse capítulo substitui a manipulação direta do DOM por componentes. Vamos reconstruir a mesma lista de eventos usando React, e no caminho adicionar o que faltava: estados de carregamento e de erro, uma tela de detalhe para cada evento, e navegação entre as telas sem recarregar a página.

De Manipulação do DOM para Componentes

De Manipulação do DOM para Componentes

No capítulo anterior, renderizar um card de evento era assim:

const card = document.createElement("article");
const title = document.createElement("h2");
title.textContent = ev.title;
const date = document.createElement("time");
date.textContent = new Date(ev.date).toLocaleString("pt-BR", {
  dateStyle: "short", timeStyle: "short",
});
// ... mais 4 createElement + textContent ...
card.append(title, date, desc, location, info, btn);
container.appendChild(card);

Com React, o mesmo card vira:

function EventCard({ event }: { event: Event }) {
  return (
    <article>
      <h2>{event.title}</h2>
      <time dateTime={event.date}>
        {new Date(event.date).toLocaleString("pt-BR", {
          dateStyle: "short",
          timeStyle: "short",
        })}
      </time>
      <p>{event.description}</p>
      <p>📍 {event.location}</p>
      <p>Vagas: {event.capacity} · Organizado por {event.organizer_name}</p>
    </article>
  );
}

Código 7-1: O mesmo card de evento, agora como componente React.

A diferença: o componente descreve o que deve aparecer na tela para um dado evento. Não diz como atualizar o DOM — não cria elementos, não busca nós, não define .textContent. Quando os dados mudam, o React chama o componente de novo, compara a descrição anterior com a nova, e aplica apenas as diferenças necessárias no DOM.

Esse é o JSX — a sintaxe que mistura JavaScript com marcação HTML. Parece HTML, mas é JavaScript: as chaves {} contêm expressões JavaScript avaliadas em tempo real. O JSX é compilado em chamadas de função que o React processa internamente. Você escreve marcação, o compilador transforma em código.

Criando o Projeto com Vite

Criando o Projeto com Vite

Para usar React com JSX, precisamos de uma ferramenta de build — algo que compile o JSX em JavaScript que o navegador entende. Vamos usar o Vite, um bundler rápido com suporte nativo a TypeScript e React.

Dentro do diretório web/ do monorepo, crie o projeto:

$ cd web
$ npm create vite@latest . -- --template react-ts

O comando cria os arquivos do projeto no diretório atual (.). O template react-ts já vem configurado com TypeScript e React. Instale as dependências:

$ npm install

E inicie o servidor de desenvolvimento:

$ npx vite
  VITE v6.3.5  ready in 312 ms

  ➜  Local:   http://localhost:5173/

Abra http://localhost:5173/ no navegador. Você verá a página padrão do Vite com o logo do React girando. Funciona — mas é o template padrão, não o TamoJunto.

Conectando na API do backend

O Vite roda na porta 5173, e o backend roda na porta 3000. São origens diferentes — o navegador bloquearia um fetch("/api/events") feito a partir da página do Vite pela mesma razão de CORS que vimos no capítulo anterior.

A solução é configurar o Vite como proxy: requisições que começam com /api são redirecionadas automaticamente para http://localhost:3000. O navegador vê a requisição como sendo da mesma origem, e o CORS não se aplica.

Abra o arquivo vite.config.ts e adicione a configuração do proxy:

Arquivo: web/vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [react()],
  server: {
    proxy: {
      "/api": "http://localhost:3000",
    },
  },
});

Código 7-2: Proxy que redireciona /api para o backend.

Com isso, fetch("/api/events") no código React funciona exatamente como funcionava quando a página era servida pelo Express — a URL relativa é a mesma, o código de fetch não muda.

Limpando o template

O Vite gera vários arquivos de demonstração que não precisamos. Substitua o conteúdo de src/App.tsx por um componente mínimo:

function App() {
  return <h1>TamoJunto</h1>;
}

export default App;

Apague os arquivos de estilo padrão (src/App.css, src/index.css) e remova os imports correspondentes de src/main.tsx. Abra o navegador: a página agora mostra apenas “TamoJunto”.

Definindo o Tipo de Evento

Definindo o Tipo de Evento

Antes de buscar dados, vamos definir o tipo TypeScript que representa um evento. Crie o arquivo src/types.ts:

Arquivo: web/src/types.ts
export interface Event {
  id: number;
  title: string;
  description: string;
  location: string;
  date: string;
  capacity: number;
  created_at: string;
  organizer_name: string;
}

Código 7-3: Tipo que espelha o JSON retornado por GET /api/events.

Esse tipo espelha exatamente os campos que o backend retorna. Se o backend mudar a forma da resposta, o TypeScript avisa nos lugares que usam esse tipo — antes de a página quebrar.

Buscando Dados no Componente

Buscando Dados no Componente

No capítulo anterior, a chamada fetch acontecia diretamente no <script>. Com React, precisamos de um mecanismo para disparar o fetch quando o componente é montado na tela e armazenar o resultado para renderização. É aqui que entram dois conceitos fundamentais do React: state e effect.

State: dados que mudam

O state é um valor que pertence ao componente e que, quando muda, faz o componente re-renderizar. Declaramos um state com o hook useState:

const [events, setEvents] = useState<Event[]>([]);

events é o valor atual (um array vazio no início). setEvents é a função que atualiza esse valor. Quando chamamos setEvents(novoArray), o React re-renderiza o componente com o novo array — e a interface na tela muda automaticamente.

Effect: código que roda no momento certo

Um effect é código que roda depois do componente ser montado no DOM. É o lugar certo para buscar dados de um servidor:

useEffect(() => {
  fetch("/api/events")
    .then((res) => res.json())
    .then((data) => setEvents(data));
}, []);

O array vazio [] no final é a lista de dependências. Ele diz ao React: “execute esse efeito uma vez, quando o componente aparece na tela, e nunca mais”. Se passássemos [filtro], o efeito rodaria toda vez que a variável filtro mudasse.

Juntando tudo: o componente EventList

Arquivo: web/src/EventList.tsx
import { useState, useEffect } from "react";
import { Event } from "./types";

function EventList() {
  const [events, setEvents] = useState<Event[]>([]);

  useEffect(() => {
    fetch("/api/events")
      .then((res) => res.json())
      .then((data) => setEvents(data));
  }, []);

  return (
    <div>
      <h1>TamoJunto — Eventos</h1>
      {events.map((ev) => (
        <article key={ev.id}>
          <h2>{ev.title}</h2>
          <time dateTime={ev.date}>
            {new Date(ev.date).toLocaleString("pt-BR", {
              dateStyle: "short",
              timeStyle: "short",
            })}
          </time>
          <p>{ev.description}</p>
          <p>📍 {ev.location}</p>
          <p>
            Vagas: {ev.capacity} · Organizado por {ev.organizer_name}
          </p>
        </article>
      ))}
    </div>
  );
}

export default EventList;

Código 7-4: Componente que busca e renderiza a lista de eventos.

O events.map(...) percorre o array e retorna um <article> para cada evento. O atributo key={ev.id} é obrigatório em listas: o React usa a key para identificar qual item mudou, foi adicionado ou removido, evitando re-renderizar a lista inteira quando um único item muda.

Atualize App.tsx para usar o componente:

import EventList from "./EventList";

function App() {
  return <EventList />;
}

export default App;

Abra o navegador (com o backend rodando em outro terminal). Os 12 eventos aparecem na tela — mesmos dados, mesma aparência, mas agora gerenciados pelo React.

Estados de Carregamento e de Erro

Estados de Carregamento e de Erro

O EventList acima tem dois problemas. Se o backend estiver fora do ar, a página fica em branco — sem mensagem, sem indicação de falha. E mesmo quando o backend responde, existe um instante entre a montagem do componente e a chegada dos dados em que a lista está vazia e nada aparece na tela.

Vamos adicionar dois estados para cobrir esses cenários:

Arquivo: web/src/EventList.tsx
import { useState, useEffect } from "react";
import { Event } from "./types";

function EventList() {
  const [events, setEvents] = useState<Event[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    fetch("/api/events")
      .then((res) => {
        if (!res.ok) throw new Error(`Erro ${res.status}`);
        return res.json();
      })
      .then((data) => setEvents(data))
      .catch((err) => setError(err.message))
      .finally(() => setLoading(false));
  }, []);

  if (loading) return <p>Carregando eventos...</p>;
  if (error) return <p>Falha ao carregar eventos: {error}</p>;

  return (
    <div>
      <h1>TamoJunto — Eventos</h1>
      {events.map((ev) => (
        <article key={ev.id}>
          <h2>{ev.title}</h2>
          <time dateTime={ev.date}>
            {new Date(ev.date).toLocaleString("pt-BR", {
              dateStyle: "short",
              timeStyle: "short",
            })}
          </time>
          <p>{ev.description}</p>
          <p>📍 {ev.location}</p>
          <p>
            Vagas: {ev.capacity} · Organizado por {ev.organizer_name}
          </p>
        </article>
      ))}
    </div>
  );
}

export default EventList;

Código 7-5: Lista de eventos com estados de carregamento e erro.

Três variáveis de estado controlam o que aparece na tela:

  • loading começa como true. Enquanto for true, o componente renderiza “Carregando eventos…”.
  • error começa como null. Se o fetch falhar — rede fora do ar, backend retornando 500 — o catch salva a mensagem de erro, e o componente renderiza a mensagem de falha.
  • events recebe os dados quando o fetch funciona.

O .finally(() => setLoading(false)) garante que o indicador de carregamento desaparece tanto no sucesso quanto na falha.

Para testar o estado de erro, pare o backend e recarregue a página:

Falha ao carregar eventos: Failed to fetch

Para testar o estado de carregamento, abra o DevTools, vá na aba Network e ative “Slow 3G”. Recarregue — “Carregando eventos…” aparece por alguns segundos antes dos dados.

Extraindo o Card em um Componente

Extraindo o Card em um Componente

O JSX dentro do events.map está ficando grande. Vamos extrair o card em seu próprio componente. Crie src/EventCard.tsx:

Arquivo: web/src/EventCard.tsx
import { Event } from "./types";

function EventCard({ event }: { event: Event }) {
  return (
    <article className="event-card">
      <h2>{event.title}</h2>
      <time dateTime={event.date}>
        {new Date(event.date).toLocaleString("pt-BR", {
          dateStyle: "short",
          timeStyle: "short",
        })}
      </time>
      <p>{event.description}</p>
      <p>📍 {event.location}</p>
      <p>
        Vagas: {event.capacity} · Organizado por {event.organizer_name}
      </p>
    </article>
  );
}

export default EventCard;

Código 7-6: Componente do card de evento.

E simplifique o EventList:

{events.map((ev) => (
  <EventCard key={ev.id} event={ev} />
))}

O EventCard recebe um evento via props — parâmetros que o componente pai passa ao filho. Props são somente leitura: o EventCard lê event.title, mas nunca modifica o objeto event. Se os dados mudarem, o componente pai re-renderiza o EventCard com novos props, e a tela se atualiza.

Essa divisão não é só estética. Se amanhã precisarmos de um card com layout diferente (uma versão compacta para uma sidebar, por exemplo), basta criar outro componente que recebe os mesmos props. A lógica de fetch fica no EventList, o layout fica no card.

Navegação entre Páginas

A lista de eventos mostra o título, a data e a descrição. Mas o TamoJunto precisa de uma tela de detalhe onde o usuário veja todas as informações do evento. Na web, cada tela tem sua própria URL: /events para a lista, /events/3 para o detalhe do evento com id = 3.

Em aplicações tradicionais (como o próprio backend com Express), cada URL dispara uma requisição HTTP ao servidor, que retorna uma página HTML nova. Com componentes, podemos fazer diferente: client-side routing. O JavaScript intercepta cliques em links, atualiza a URL no navegador, e renderiza o componente correspondente — sem sair da página.

Instalando o React Router

$ npm install react-router-dom

O React Router é a biblioteca de roteamento mais usada com React. Ela oferece componentes que mapeiam URLs a componentes da aplicação.

Configurando as rotas

Atualize App.tsx:

Arquivo: web/src/App.tsx
import { BrowserRouter, Routes, Route } from "react-router-dom";
import EventList from "./EventList";
import EventDetail from "./EventDetail";

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<EventList />} />
        <Route path="/events/:id" element={<EventDetail />} />
      </Routes>
    </BrowserRouter>
  );
}

export default App;

Código 7-7: Rotas da aplicação: lista na raiz, detalhe em /events/:id.

Duas rotas. O / renderiza a lista de eventos. O /events/:id renderiza o detalhe — o :id é um parâmetro de rota, uma parte dinâmica da URL que o componente pode ler. Acessar /events/3 renderiza o EventDetail com id = "3".

Linkando da lista para o detalhe

No EventCard, transforme o título num link:

Arquivo: web/src/EventCard.tsx
import { Link } from "react-router-dom";
import { Event } from "./types";

function EventCard({ event }: { event: Event }) {
  return (
    <article className="event-card">
      <h2>
        <Link to={`/events/${event.id}`}>{event.title}</Link>
      </h2>
      <time dateTime={event.date}>
        {new Date(event.date).toLocaleString("pt-BR", {
          dateStyle: "short",
          timeStyle: "short",
        })}
      </time>
      <p>{event.description}</p>
      <p>📍 {event.location}</p>
      <p>
        Vagas: {event.capacity} · Organizado por {event.organizer_name}
      </p>
    </article>
  );
}

export default EventCard;

Código 7-8: Título do evento como link para a tela de detalhe.

O <Link> do React Router renderiza um <a> no HTML, mas intercepta o clique: ao invés de navegar para o servidor, ele atualiza a URL no navegador e renderiza o componente da rota correspondente. O botão “voltar” do navegador funciona normalmente — o React Router gerencia o histórico.

Tela de Detalhe

Tela de Detalhe

O EventDetail precisa buscar os dados de um único evento. O backend ainda não tem um endpoint GET /api/events/:id — ele retorna a lista completa. Então vamos buscar a lista e filtrar pelo id no frontend1.

Crie src/EventDetail.tsx:

Arquivo: web/src/EventDetail.tsx
import { useState, useEffect } from "react";
import { useParams, Link } from "react-router-dom";
import { Event } from "./types";

function EventDetail() {
  const { id } = useParams<{ id: string }>();
  const [event, setEvent] = useState<Event | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    fetch("/api/events")
      .then((res) => {
        if (!res.ok) throw new Error(`Erro ${res.status}`);
        return res.json();
      })
      .then((data: Event[]) => {
        const found = data.find((ev) => ev.id === Number(id));
        if (!found) throw new Error("Evento não encontrado");
        setEvent(found);
      })
      .catch((err) => setError(err.message))
      .finally(() => setLoading(false));
  }, [id]);

  if (loading) return <p>Carregando...</p>;
  if (error) return <p>{error}</p>;
  if (!event) return null;

  return (
    <div>
      <Link to="/">← Voltar</Link>
      <h1>{event.title}</h1>
      <time dateTime={event.date}>
        {new Date(event.date).toLocaleString("pt-BR", {
          dateStyle: "full",
          timeStyle: "short",
        })}
      </time>
      <p>{event.description}</p>
      <p>📍 {event.location}</p>
      <p>Capacidade: {event.capacity} pessoas</p>
      <p>Organizado por {event.organizer_name}</p>
    </div>
  );
}

export default EventDetail;

Código 7-9: Tela de detalhe que exibe um único evento.

O useParams extrai o id da URL. O useEffect tem [id] na lista de dependências — se o usuário navegar de /events/3 para /events/7 sem voltar à lista, o efeito roda novamente com o novo id.

A data aqui usa dateStyle: "full" para mostrar o formato extenso: “segunda- feira, 15 de setembro de 2026, 08:00”. A lista usava o formato curto. Cada tela mostra o nível de detalhe adequado.


  1. Buscar a lista inteira para exibir um evento é desperdício de banda e processamento. O exercício 6 do capítulo Armazenando e Servindo Dados propõe criar o endpoint GET /api/events/:id. Se você o implementou, use-o aqui em vez de buscar a lista completa. ↩

Estilizando os Componentes

Estilizando os Componentes

Os componentes estão funcionais mas sem nenhum CSS. Crie um arquivo src/App.css com os estilos da aplicação e importe-o no main.tsx:

Arquivo: web/src/App.css
body {
  font-family: system-ui, sans-serif;
  background: #f7fafc;
  color: #1a202c;
  max-width: 800px;
  margin: 0 auto;
  padding: 2rem 1rem;
}

.event-list {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
}

.event-card {
  background: #fff;
  border: 1px solid #e2e8f0;
  border-radius: 8px;
  padding: 1.5rem;
  flex: 1 1 320px;
}

.event-card h2 {
  margin: 0 0 0.5rem;
  font-size: 1.3rem;
}

.event-card h2 a {
  color: #2b6cb0;
  text-decoration: none;
}

.event-card h2 a:hover {
  text-decoration: underline;
}

.event-card time {
  display: block;
  color: #718096;
  font-size: 0.9rem;
  margin-bottom: 0.75rem;
}

.event-card p {
  margin: 0.25rem 0;
  font-size: 0.95rem;
}

Código 7-10: Estilos globais da aplicação.

E no EventList, envolva os cards num contêiner com a classe:

<div className="event-list">
  {events.map((ev) => (
    <EventCard key={ev.id} event={ev} />
  ))}
</div>

Repare no className em vez de class. Em JSX, class é uma palavra reservada do JavaScript, então o React usa className para o atributo CSS. É uma das poucas diferenças entre JSX e HTML.

O que Mudou

O que Mudou

Compare o ponto de partida — manipulação direta do DOM, createElement para cada campo, atualização manual de cada elemento — com o resultado:

  • Renderização: o componente descreve a interface como função dos dados. O React calcula as atualizações.
  • Fetch e estados: useState e useEffect organizam o ciclo de carregamento, sucesso e erro em poucas linhas.
  • Navegação: o React Router mapeia URLs a componentes. Clicar num link troca a tela sem recarregar a página.
  • Composição: EventList usa EventCard. App usa EventList e EventDetail. Cada componente é uma peça isolada com responsabilidade clara.

O código que o programador escreve mudou de caráter. Não é mais um script imperativo que busca nós e os modifica um a um. É uma descrição declarativa de como a interface deve ser — e o framework cuida do resto.

Exercícios e Referências

Exercícios

  1. Adicione um campo “Criado em” ao EventDetail que mostre a data de criação do evento formatada com toLocaleDateString("pt-BR"). Não adicione esse campo ao EventCard — é informação demais para a lista.

  2. O EventList atual mostra “Carregando eventos…” como texto puro. Substitua por um indicador mais visível: um <div> centralizado com fonte maior e cor cinza. Faça o mesmo para a mensagem de erro, usando uma cor vermelha.

  3. Crie uma página “Não encontrado” que é renderizada quando o usuário acessa uma URL que não corresponde a nenhuma rota (ex: /sobre). Use a rota path="*" no React Router.

  4. O EventDetail busca a lista inteira e filtra pelo id. Se o backend tivesse um endpoint GET /api/events/:id (exercício 6 do capítulo Armazenando e Servindo Dados), como o código mudaria? Reescreva o useEffect para usar esse endpoint hipotético e compare.

  5. Implemente um botão “RSVP” no EventDetail. Ao clicar, ele muda de “RSVP” para “Confirmado ✓” (com cor verde) usando useState. Esse estado é local — recarregar a página zera o botão. Persistir no servidor é tarefa de um capítulo futuro.

  6. Adicione um <input> de busca no EventList. Use useState para armazenar o texto digitado e filtre o array events com .filter() antes do .map(). O filtro deve buscar no título e na descrição do evento (case-insensitive).

Referências

A documentação oficial do React é interativa e cobre todos os conceitos desse capítulo com exemplos editáveis: https://react.dev/learn.

Para o React Router, a documentação com exemplos e guias de migração está em: https://reactrouter.com/.

A documentação do Vite explica a configuração de proxy e outras opções do servidor de desenvolvimento: https://vite.dev/config/server-options.

Iniciando o Frontend Mobile

A lista de eventos do TamoJunto já funciona no navegador — tanto na versão com manipulação direta do DOM quanto na versão com componentes React. Mas uma plataforma de eventos precisa estar no bolso do usuário. Ninguém vai abrir o laptop para conferir se o hackathon mudou de sala.

Esse capítulo constrói o frontend mobile do TamoJunto. Vamos exibir a mesma lista de eventos que o backend já serve, mas num aplicativo que roda em Android e iOS. No caminho, entendemos por que aplicativos mobile não são “uma página web numa tela menor” — o modelo de navegação, o tratamento de listas longas e até a forma como os pixels são desenhados são fundamentalmente diferentes.

Nativo, Web ou Multiplataforma?

Nativo, Web ou Multiplataforma?

Existem três abordagens para construir um aplicativo mobile:

Nativo — código escrito na linguagem da plataforma: Kotlin ou Java para Android, Swift ou Objective-C para iOS. Acesso direto a todas as APIs do sistema operacional. O custo: duas codebases completamente separadas para a mesma funcionalidade.

Web (PWA) — uma aplicação web empacotada para funcionar offline e ser instalada na tela inicial. Usa HTML, CSS e JavaScript, rodando dentro do motor do navegador. Funciona nas duas plataformas com uma codebase só, mas fica limitada ao que o navegador permite — notificações push, acesso à câmera e sensores dependem do suporte do browser, que varia bastante entre Android e iOS.

Multiplataforma — frameworks como React Native, Flutter e Kotlin Multiplatform permitem escrever uma codebase que gera aplicativos nativos para ambas as plataformas. O trade-off é diferente de um PWA: o código roda fora do navegador, com acesso a APIs nativas, mas cada framework tem suas limitações e peculiaridades.

Neste livro, vamos usar React Native com Expo. O React Native traduz componentes escritos em JavaScript/TypeScript para views nativas de cada plataforma — um <View> vira um android.view.View no Android e uma UIView no iOS. O Expo adiciona uma camada de ferramentas que simplifica o setup: não precisa instalar Android Studio nem Xcode para começar a desenvolver.

A escolha é pragmática. O TamoJunto já usa React no frontend web — reutilizamos a mesma linguagem (TypeScript), os mesmos conceitos (componentes, hooks, state, props) e parte da mesma lógica de negócio. Um desenvolvedor que acabou de construir o EventList em React vai reconhecer imediatamente a estrutura do código mobile.

Criando o Projeto com Expo

Criando o Projeto com Expo

Dentro do diretório mobile/ do monorepo:

$ cd mobile
$ npx create-expo-app@latest . --template blank-typescript

O template blank-typescript cria um projeto mínimo com TypeScript. Instale as dependências:

$ npm install

Para rodar, o Expo oferece várias opções. A mais simples é usar o aplicativo Expo Go no seu celular — ele carrega o projeto via rede local sem precisar compilar um APK ou IPA:

$ npx expo start
Metro waiting on exp://192.168.1.42:8081
› Scan the QR code above with Expo Go (Android) or the Camera app (iOS)

› Using Expo Go
› Press a │ open Android
› Press i │ open iOS simulator
› Press w │ open web

Escaneie o QR code com o Expo Go no celular. A tela padrão com “Open up App.tsx to start working on your app!” aparece no dispositivo. O Metro Bundler (o compilador do React Native) fica observando alterações — salve um arquivo e o app atualiza em segundos.

Se preferir usar um emulador, pressione a para Android (requer Android Studio instalado) ou i para iOS (requer Xcode, apenas no macOS).

Conectando na API

Conectando na API

No navegador, fetch("/api/events") funciona porque a página está na mesma origem que o backend. No mobile, a situação é diferente. O aplicativo não roda num navegador — não existe “mesma origem”. A URL precisa ser absoluta.

E tem um detalhe: http://localhost:3000 não funciona no emulador Android. O localhost do emulador é o próprio emulador, não a sua máquina. O Android oferece o endereço especial 10.0.2.2 para acessar o localhost do host. No iOS Simulator, localhost funciona porque o simulador compartilha a rede com o macOS. E num celular físico, você precisa do IP da sua máquina na rede local.

Para não ficar alterando a URL manualmente, crie um arquivo de configuração. Crie src/config.ts:

Arquivo: mobile/src/config.ts
import { Platform } from "react-native";

function getBaseUrl(): string {
  if (Platform.OS === "android") {
    return "http://10.0.2.2:3000";
  }
  return "http://localhost:3000";
}

export const API_BASE_URL = getBaseUrl();

Código 8-1: URL base da API com tratamento por plataforma.

O Platform.OS retorna "android" ou "ios" dependendo de onde o app está rodando. Se estiver testando num celular físico, substitua o retorno pelo IP da sua máquina (ex: "http://192.168.1.42:3000").

Uma vantagem: como o React Native não roda no navegador, não existe CORS. A política de mesma origem é uma restrição do browser — aplicativos nativos fazem requisições HTTP para qualquer URL sem restrições.

O Tipo Compartilhado

O Tipo Compartilhado

O tipo Event que criamos no projeto web descreve a forma do JSON retornado pelo backend. O mobile consome a mesma API, então o tipo é idêntico. Crie src/types.ts:

Arquivo: mobile/src/types.ts
export interface Event {
  id: number;
  title: string;
  description: string;
  location: string;
  date: string;
  capacity: number;
  created_at: string;
  organizer_name: string;
}

Código 8-2: Mesmo tipo do projeto web — mesma API, mesma forma de dados.

O fato de estarmos duplicando esse arquivo é intencional. Poderíamos compartilhá-lo entre web/ e mobile/ usando npm workspaces ou um pacote local. Mas isso adiciona complexidade no build que não se justifica por um único arquivo. Quando a duplicação crescer — tipos de requisição, funções de formatação, validadores — aí sim faz sentido extrair um pacote compartilhado1.


  1. npm workspaces permitem que pacotes dentro de um monorepo se referenciem sem publicar no registry. A configuração envolve um package.json na raiz com "workspaces": ["backend", "web", "mobile", "shared"] e um pacote shared/ com seu próprio package.json. O custo é lidar com resolução de módulos entre pacotes, versões do TypeScript e configuração de build — justificável quando a duplicação cresce, excessivo para um arquivo. ↩

Lista de Eventos com FlatList

Lista de Eventos com FlatList

No React para web, renderizar uma lista é events.map(...). No React Native, listas grandes usam o componente FlatList. A razão: numa lista com 500 itens, o map renderiza todos os 500 de uma vez. A FlatList renderiza apenas os itens visíveis na tela e recicla as views conforme o usuário rola — o equivalente a RecyclerView no Android ou UICollectionView no iOS.

Com 12 eventos, a diferença de performance é imperceptível. Mas o hábito de usar FlatList desde o início evita a refatoração quando a lista crescer.

Crie src/EventListScreen.tsx:

Arquivo: mobile/src/EventListScreen.tsx
import { useState, useEffect } from "react";
import {
  View,
  Text,
  FlatList,
  StyleSheet,
  ActivityIndicator,
} from "react-native";
import { Event } from "./types";
import { API_BASE_URL } from "./config";

export default function EventListScreen() {
  const [events, setEvents] = useState<Event[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    fetch(`${API_BASE_URL}/api/events`)
      .then((res) => {
        if (!res.ok) throw new Error(`Erro ${res.status}`);
        return res.json();
      })
      .then((data) => setEvents(data))
      .catch((err) => setError(err.message))
      .finally(() => setLoading(false));
  }, []);

  if (loading) {
    return (
      <View style={styles.center}>
        <ActivityIndicator size="large" color="#3182ce" />
      </View>
    );
  }

  if (error) {
    return (
      <View style={styles.center}>
        <Text style={styles.error}>{error}</Text>
      </View>
    );
  }

  return (
    <FlatList
      data={events}
      keyExtractor={(item) => String(item.id)}
      contentContainerStyle={styles.list}
      renderItem={({ item }) => (
        <View style={styles.card}>
          <Text style={styles.title}>{item.title}</Text>
          <Text style={styles.date}>
            {new Date(item.date).toLocaleString("pt-BR", {
              dateStyle: "short",
              timeStyle: "short",
            })}
          </Text>
          <Text style={styles.description}>{item.description}</Text>
          <Text style={styles.location}>📍 {item.location}</Text>
          <Text style={styles.info}>
            Vagas: {item.capacity} · {item.organizer_name}
          </Text>
        </View>
      )}
    />
  );
}

const styles = StyleSheet.create({
  center: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
  },
  list: {
    padding: 16,
  },
  card: {
    backgroundColor: "#fff",
    borderRadius: 8,
    padding: 16,
    marginBottom: 12,
    borderWidth: 1,
    borderColor: "#e2e8f0",
  },
  title: {
    fontSize: 18,
    fontWeight: "600",
    marginBottom: 4,
  },
  date: {
    color: "#718096",
    fontSize: 14,
    marginBottom: 8,
  },
  description: {
    fontSize: 15,
    marginBottom: 4,
  },
  location: {
    fontSize: 14,
    marginBottom: 4,
  },
  info: {
    fontSize: 14,
    color: "#4a5568",
  },
  error: {
    color: "#e53e3e",
    fontSize: 16,
  },
});

Código 8-3: Lista de eventos usando FlatList.

A FlatList recebe três props obrigatórios:

  • data — o array de itens.
  • keyExtractor — uma função que retorna um identificador único para cada item (equivalente ao key no React web).
  • renderItem — uma função que recebe um item e retorna o componente a ser renderizado.

Estilos no React Native

CSS não existe no React Native. Em vez de seletores e cascata, cada componente recebe estilos via objetos JavaScript criados com StyleSheet.create. As propriedades são camelCase (backgroundColor em vez de background-color) e os valores são números (pixels) ou strings.

Não existe display: block nem display: inline. Todo contêiner é flex por padrão — flexDirection começa como "column" (diferente da web, onde o padrão é "row"). Isso faz sentido: telas de celular são verticais, e a maioria dos layouts mobile é uma coluna de elementos empilhados.

Navegação entre Telas

Na web, a navegação é baseada em URLs. No mobile, o modelo é diferente: telas são empilhadas umas sobre as outras, como cartas num baralho. Quando o usuário toca num evento, a tela de detalhe entra por cima da lista. O botão “voltar” remove a tela do topo, revelando a anterior.

Esse modelo se chama stack navigation. O React Native não tem navegação embutida — vamos instalar o React Navigation:

$ npx expo install @react-navigation/native @react-navigation/native-stack
$ npx expo install react-native-screens react-native-safe-area-context

O expo install garante que as versões das bibliotecas são compatíveis com a versão do Expo do projeto.

Configurando o stack navigator

Crie src/navigation.tsx:

Arquivo: mobile/src/navigation.tsx
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import EventListScreen from "./EventListScreen";
import EventDetailScreen from "./EventDetailScreen";

export type RootStackParamList = {
  EventList: undefined;
  EventDetail: { eventId: number };
};

const Stack = createNativeStackNavigator<RootStackParamList>();

export default function AppNavigator() {
  return (
    <Stack.Navigator>
      <Stack.Screen
        name="EventList"
        component={EventListScreen}
        options={{ title: "TamoJunto" }}
      />
      <Stack.Screen
        name="EventDetail"
        component={EventDetailScreen}
        options={{ title: "Detalhe do Evento" }}
      />
    </Stack.Navigator>
  );
}

Código 8-4: Stack navigator com duas telas.

O RootStackParamList define os tipos das rotas: EventList não recebe parâmetros, EventDetail recebe um eventId. O TypeScript garante que, ao navegar para EventDetail, o parâmetro eventId é obrigatório — se esquecer, o compilador reclama.

Atualize o App.tsx na raiz do projeto:

Arquivo: mobile/App.tsx
import { NavigationContainer } from "@react-navigation/native";
import AppNavigator from "./src/navigation";

export default function App() {
  return (
    <NavigationContainer>
      <AppNavigator />
    </NavigationContainer>
  );
}

Código 8-5: Ponto de entrada com o container de navegação.

No EventListScreen, cada card precisa ser tocável. Envolva o card com TouchableOpacity e use navigation.navigate para trocar de tela:

import { TouchableOpacity } from "react-native";
import { useNavigation } from "@react-navigation/native";
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
import { RootStackParamList } from "./navigation";

type NavigationProp = NativeStackNavigationProp<RootStackParamList, "EventList">;

export default function EventListScreen() {
  const navigation = useNavigation<NavigationProp>();
  // ... useState, useEffect como antes ...

  return (
    <FlatList
      data={events}
      keyExtractor={(item) => String(item.id)}
      contentContainerStyle={styles.list}
      renderItem={({ item }) => (
        <TouchableOpacity
          onPress={() =>
            navigation.navigate("EventDetail", { eventId: item.id })
          }
        >
          <View style={styles.card}>
            <Text style={styles.title}>{item.title}</Text>
            {/* ... restante do card ... */}
          </View>
        </TouchableOpacity>
      )}
    />
  );
}

Código 8-6: Card tocável que navega para a tela de detalhe.

O TouchableOpacity diminui a opacidade do card quando pressionado, dando feedback visual ao toque — o equivalente ao :hover da web, mas para interação por toque.

Tela de Detalhe

Tela de Detalhe

Crie src/EventDetailScreen.tsx:

Arquivo: mobile/src/EventDetailScreen.tsx
import { useState, useEffect } from "react";
import { View, Text, StyleSheet, ActivityIndicator } from "react-native";
import { RouteProp, useRoute } from "@react-navigation/native";
import { Event } from "./types";
import { API_BASE_URL } from "./config";
import { RootStackParamList } from "./navigation";

type DetailRoute = RouteProp<RootStackParamList, "EventDetail">;

export default function EventDetailScreen() {
  const route = useRoute<DetailRoute>();
  const { eventId } = route.params;

  const [event, setEvent] = useState<Event | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    fetch(`${API_BASE_URL}/api/events`)
      .then((res) => {
        if (!res.ok) throw new Error(`Erro ${res.status}`);
        return res.json();
      })
      .then((data: Event[]) => {
        const found = data.find((ev) => ev.id === eventId);
        if (!found) throw new Error("Evento não encontrado");
        setEvent(found);
      })
      .catch((err) => setError(err.message))
      .finally(() => setLoading(false));
  }, [eventId]);

  if (loading) {
    return (
      <View style={styles.center}>
        <ActivityIndicator size="large" color="#3182ce" />
      </View>
    );
  }

  if (error) {
    return (
      <View style={styles.center}>
        <Text style={styles.error}>{error}</Text>
      </View>
    );
  }

  if (!event) return null;

  return (
    <View style={styles.container}>
      <Text style={styles.title}>{event.title}</Text>
      <Text style={styles.date}>
        {new Date(event.date).toLocaleString("pt-BR", {
          dateStyle: "full",
          timeStyle: "short",
        })}
      </Text>
      <Text style={styles.description}>{event.description}</Text>
      <Text style={styles.location}>📍 {event.location}</Text>
      <Text style={styles.info}>
        Capacidade: {event.capacity} pessoas
      </Text>
      <Text style={styles.info}>
        Organizado por {event.organizer_name}
      </Text>
    </View>
  );
}

const styles = StyleSheet.create({
  center: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
  },
  container: {
    padding: 20,
  },
  title: {
    fontSize: 24,
    fontWeight: "700",
    marginBottom: 8,
  },
  date: {
    color: "#718096",
    fontSize: 15,
    marginBottom: 16,
  },
  description: {
    fontSize: 16,
    lineHeight: 24,
    marginBottom: 16,
  },
  location: {
    fontSize: 15,
    marginBottom: 8,
  },
  info: {
    fontSize: 15,
    color: "#4a5568",
    marginBottom: 4,
  },
  error: {
    color: "#e53e3e",
    fontSize: 16,
  },
});

Código 8-7: Tela de detalhe do evento.

O useRoute extrai os parâmetros da rota — nesse caso, o eventId. O resto do padrão (fetch, loading, error) é o mesmo do EventList. A diferença é visual: a tela de detalhe usa fontes maiores e mais espaçamento, já que dispõe da tela inteira em vez de dividir espaço com outros cards.

O botão “voltar” aparece automaticamente. O stack navigator coloca uma seta no canto superior esquerdo de qualquer tela que não seja a primeira da pilha. No Android, o botão de voltar do sistema (gesto de swipe ou botão virtual) também funciona sem nenhum código adicional.

Telas, Pixels e Densidade

Telas, Pixels e Densidade

Uma questão que não existe na web: a mesma imagem de 300×200 pixels parece nítida num celular barato e borrada num flagship. A razão é a densidade de pixels — a quantidade de pixels físicos por ponto lógico da tela.

Um celular com densidade 1x tem 1 pixel físico por ponto. Um com 2x tem 4 (2×2). Um com 3x tem 9 (3×3). Quando você define width: 300 no React Native, são 300 pontos lógicos — que se traduzem em 300 pixels num dispositivo 1x, 600 num 2x e 900 num 3x.

Para texto e formas vetoriais, isso é transparente — o sistema operacional renderiza em alta resolução automaticamente. Para imagens bitmap (fotos, ícones PNG), a imagem precisa existir em múltiplas resoluções. Uma imagem logo.png de 100×100 pixels ficará borrada num dispositivo 3x que precisa preencher 300×300 pixels físicos.

A convenção no React Native é fornecer três versões:

assets/
├── logo.png       (100×100 — para 1x)
├── logo@2x.png    (200×200 — para 2x)
└── logo@3x.png    (300×300 — para 3x)

O Metro Bundler seleciona automaticamente a versão correta com base na densidade do dispositivo. No código, basta referenciar require("./assets/logo.png") — o sufixo @2x/@3x é resolvido em tempo de build1.

O TamoJunto ainda não tem imagens nos cards de evento — capas de evento aparecem quando o upload de imagem for implementado. Mas entender densidades agora evita surpresas quando elas chegarem.


  1. A API PixelRatio.get() do React Native retorna a densidade do dispositivo atual. Útil quando você precisa calcular tamanhos em pixels físicos — por exemplo, para definir a espessura mínima de uma borda que precisa ser exatamente 1 pixel físico: borderWidth: 1 / PixelRatio.get(). ↩

Lógica Compartilhada entre Web e Mobile

Lógica Compartilhada entre Web e Mobile

Os componentes EventList no web e EventListScreen no mobile têm algo em comum: ambos fazem fetch para /api/events, armazenam o resultado em state, tratam loading e erro. A lógica de UI é diferente (<article> vs <View>, CSS vs StyleSheet), mas a lógica de dados é idêntica.

Podemos extrair essa lógica num custom hook. No projeto mobile, crie src/useEvents.ts:

Arquivo: mobile/src/useEvents.ts
import { useState, useEffect } from "react";
import { Event } from "./types";

export function useEvents(baseUrl: string) {
  const [events, setEvents] = useState<Event[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    fetch(`${baseUrl}/api/events`)
      .then((res) => {
        if (!res.ok) throw new Error(`Erro ${res.status}`);
        return res.json();
      })
      .then((data) => setEvents(data))
      .catch((err) => setError(err.message))
      .finally(() => setLoading(false));
  }, [baseUrl]);

  return { events, loading, error };
}

Código 8-8: Hook que encapsula o fetch de eventos.

O EventListScreen fica mais enxuto:

import { useEvents } from "./useEvents";
import { API_BASE_URL } from "./config";

export default function EventListScreen() {
  const { events, loading, error } = useEvents(API_BASE_URL);

  if (loading) { /* ... */ }
  if (error) { /* ... */ }

  return (
    <FlatList
      data={events}
      keyExtractor={(item) => String(item.id)}
      renderItem={({ item }) => (/* ... */)}
    />
  );
}

O mesmo hook funciona no projeto web — basta copiar useEvents.ts para web/src/ e chamar useEvents("") (string vazia, porque no web o fetch usa URLs relativas via proxy). A lógica de dados vive num lugar só; cada plataforma cuida apenas da sua interface.

Esse é o nível de compartilhamento que faz sentido agora: um hook copiado entre dois projetos. Quando o TamoJunto crescer e o número de hooks compartilhados justificar a complexidade, podemos extrair um pacote shared/ no monorepo.

Exercícios e Referências

Exercícios

  1. Adicione um ActivityIndicator dentro de cada card enquanto a imagem (futura) do evento carregaria. Por enquanto, simule com um setTimeout de 1 segundo no useEffect antes de setar loading como false. Observe como o feedback visual muda a percepção de velocidade.

  2. O stack navigator mostra “Detalhe do Evento” como título fixo. Modifique para que o título da tela seja o nome do evento. Dica: use a prop options como função no Stack.Screen, que recebe route.params.

  3. Adicione um RefreshControl na FlatList para permitir pull-to-refresh. Quando o usuário puxar a lista para baixo, refaça o fetch e atualize os dados. Consulte a documentação do FlatList no React Native.

  4. O EventDetailScreen busca a lista inteira e filtra pelo id, assim como a versão web. Uma alternativa é passar o evento inteiro como parâmetro de navegação (em route.params), evitando um segundo fetch. Implemente essa abordagem e compare: quando ela é suficiente? Quando ela falha?

  5. Crie uma tab bar na parte inferior da tela com duas abas: “Eventos” (a lista atual) e “Sobre” (uma tela estática com o nome e a descrição do TamoJunto). Use @react-navigation/bottom-tabs. A aba “Eventos” deve conter o stack navigator, preservando a navegação lista → detalhe.

  6. Copie o hook useEvents.ts para o projeto web e refatore o EventList.tsx para usá-lo. Compare o código antes e depois: o que está duplicado entre web e mobile? O que é genuinamente diferente?

Referências

A documentação do React Native cobre componentes, APIs e guias de plataforma: https://reactnative.dev/docs/getting-started.

A documentação do Expo tem guias de setup e referências de todas as bibliotecas incluídas: https://docs.expo.dev/.

O React Navigation documenta todos os tipos de navegador (stack, tab, drawer) com exemplos interativos: https://reactnavigation.org/docs/getting-started.