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:
-
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.
-
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.
-
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:
- Seu celular toca o alarme e te acorda.
- 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.
- 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.
- 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.
- 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.
- 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 é
01110110001que pode representar umOLÁ, por exemplo.
- Como as partes são computadores, esse conteúdo é sempre em binário (1s e
0s). Então uma mensagem possível é
- 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):
- 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.
- 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).
- 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.
- 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).
- 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
Atem o endereço de rede10.10.3.47. Ele está se comunicando com dois outros hosts:B(cujo endereço é10.10.3.3) eC(cujo endereço é10.10.5.98). - Sempre que
Aquiser mandar uma mensagem paraB, ele manda um pacote cujo endereço de destino é10.10.3.3.Cnã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 é
wwwe o IP é10.100.3.2. - Um host para o seu serviço de e-mail. O hostname do host é
maile o IP é10.100.3.3. - Um host para o seu serviço de API2. O hostname do host é
apie 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.comrefere-se ao host que oferece o serviço do site do Google. - O FQDN
g1.globo.comrefere-se ao host que oferece o serviço g1 da Globo. - O FQDN
ge.globo.comrefere-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.
-
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. ↩
-
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).
- Um método (
-
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.ukespecifica o host sendo acessado3. Accept: text/htmlespecifica 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-BRespecifica que o conteúdo deve ser retornado preferencialmente em Português do Brasil.User-Agent: Chrome/127.0.0.0indica que o cliente acessou o servidor por meio do navegador Google Chrome na versão127.0.0.0. O valor desse header muda a depender do programa usado para acessar o site, podendo ser desde navegadores até ferramentas CLI comocurl.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-aliveespecifica que o clienter quer re-utilizar a mesma conexão TCP, re-aproveitando as configurações já feitas.
- O header
-
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
/blogpode ser usado para representar todas as postagens do blog de um determinado site. - O path
/blog/2026pode ser usado para representar todas as postagens do blog do ano de 2026. - O path
/blog/2026/08/relatorio-clima.htmlpode 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.pngrepresenta 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/42representa 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=2especifica que deve ser retornada a segunda página de resultados.sort=dateespecifica que os resultados devem vir ordenados pela data.highlight=temperaturaespecifica que o campotemperaturadeve 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.1GET /search?q=openai&lang=en HTTP/1.1POST /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 parnome=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.Host: example.comé o host que atua como servidor HTTP.Content-Type: application/jsoné um header que especifica qual o tipo do conteúdo que está no payload. Nesse caso,application/jsonindica que o payload segue o formato JSON, que veremos mais a frente.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.
- A versão do protocolo (
- 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 GMTindica o horário que o servidor mandou a resposta.Server: Nginx/1.25.2indica qual o programa ou biblioteca utilizado pelo servidor para enviar respostas HTTP.Content-Type: text/html; charset=utf-8indica que o corpo da resposta vem no formato HTML e foi codificado utilizando o padrão UTF-8.Content-Lenght: 171indica que o corpo da resposta tem 171 bytes.Connection: keep-aliveindica 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:
| Categoria | Tipo | Exemplos |
|---|---|---|
1xx | Informação | 100 Continue 101 Switching Protocols |
2xx | Sucesso | 200 OK 201 Created |
3xx | Redirecionamento | 301 Moved Permanently 302 Found |
4xx | Erro do Cliente | 400 Bad Request 403 Forbidden 404 Not Found |
5xx | Erro do Servidor | 500 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.
-
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. ↩
-
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. ↩ -
Os mais atentos podem estar se perguntando qual a necessidade do header
Hostse, para iniciar a comunicação HTTP, o cliente precisa saber antecipadamente qual o host que atuará como servidor. O motivo é que o headerHosté 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
-
Abra um terminal (no Windows, aperte
Win + R, escrevacmde aperteOk) 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.brEsse comando irá mostrar uma mensagem HTTP. Responda:
- O tipo da mensagem é uma resposta ou requisição HTTP?
- Qual o status code?
- Quantos e quais os headers?
- Essa mensagem tem corpo? Se sim, qual seu tamanho em bytes?
- O que o corpo diz? Como você mudaria o comando para atender o que o corpo diz?
-
No seu computador, abra um navegador web (Chrome, Firefox, Edge, Brave) e abra uma nova aba. Aperte
F12(ouCtrl + Shift + I), irá aparecer um painel com várias informações. Na parte superior desse painel, tem várias abas. Clique na aba chamadaRede/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.come aperte Enter.Responda:
- Quantas linhas apareceram?
- 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? - No mesmo painel, determine se a primeira requisição teve uma payload não-vazia. Faça o mesmo para a resposta.
-
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:
- 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.
- Colaboração. Várias pessoas podem trabalhar nos mesmos arquivos ao mesmo tempo sem sobrescrever o trabalho umas das outras.
- 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
aptdentro 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:
- Staging (preparação): você marca quais alterações deseja incluir no próximo registro.
- 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ê.
-
Historicamente, a branch padrão se chamava
master. Em 2020, o GitHub e outras plataformas mudaram o padrão paramain. 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:
- O desenvolvedor cria uma branch a partir da
mainpara trabalhar numa funcionalidade específica (ex:adicionar-pagina-contato). - Ele faz seus commits nessa branch.
- Quando a funcionalidade está pronta, ele envia a branch para o remoto
(
git push origin adicionar-pagina-contato). - Ele abre um pull request (PR) no GitHub, que é uma proposta de
incorporar as alterações dessa branch na
main. - Outros membros da equipe revisam o código, fazem comentários e sugerem mudanças.
- 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..envignora o arquivo de variáveis de ambiente (que costuma conter senhas).*.logignora 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 emgithub.com/equipe/web, o mobile emgithub.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
-
Instale o Git na sua máquina e configure seu nome e e-mail usando
git config. Verifique comgit config --global --listque as informações estão corretas. -
Crie um diretório chamado
exercicio-git, inicialize um repositório Git nele (git init) e crie um arquivonotas.txtcom qualquer conteúdo. Faça um commit com a mensagem “Adicionar arquivo de notas”. Verifique o histórico comgit log --oneline. -
No mesmo repositório, crie uma branch chamada
experimento. Nela, modifique o arquivonotas.txte faça um commit. Depois, volte àmaine faça uma modificação diferente na mesma linha denotas.txt. Tente fazer o merge. Resolva o conflito que surgir. -
Crie um repositório no GitHub (ou GitLab). Conecte seu repositório local a ele com
git remote add origin <URL>. Envie seus commits comgit push. Verifique no site que os arquivos aparecem. -
O arquivo
.gitignoreabaixo 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:
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 pastanode_modules/, e nenhuma delas deve ir para o repositório (são grandes e reproduzíveis vianpm 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
-
Crie o repositório do seu projeto seguindo a mesma estrutura de monorepo. Use
git init, crie os diretóriosbackend/,web/emobile/com.gitkeep, configure o.gitignoree faça o primeiro commit. -
Adicione um arquivo
README.mdna 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. -
Publique o repositório no GitHub: crie um repositório vazio, adicione o remote com
git remote add origin <URL>e envie seus commits comgit push -u origin main. Verifique no GitHub que os diretórios e o.gitignoreaparecem. -
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.
| Coluna | Tipo | Descrição |
|---|---|---|
id | SERIAL | Identificador único, gerado automaticamente |
title | VARCHAR(200) | Título do evento |
description | TEXT | Descrição detalhada |
location | VARCHAR(200) | Local onde o evento acontece |
date | TIMESTAMP | Data e horário do evento |
capacity | INTEGER | Número máximo de participantes |
organizer_id | INTEGER | Quem criou o evento (referência à tabela users) |
created_at | TIMESTAMP | Quando 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:
| Coluna | Tipo | Descrição |
|---|---|---|
id | SERIAL | Identificador único |
name | VARCHAR(100) | Nome do usuário |
email | VARCHAR(200) | E-mail (único por usuário) |
created_at | TIMESTAMP | Quando 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:
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 KEYgera um inteiro auto-incrementado e o marca como chave primária — cada registro recebe umidúnico automaticamente.NOT NULLimpede 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()preenchecreated_atautomaticamente 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.
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:
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)
-
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:
-- 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:
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/:
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
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
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
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
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
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
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:
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.
-
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
-
Adicione uma coluna
statusà tabelaevents(valores possíveis:scheduled,cancelled,completed). Escreva a migration SQL. Qual tipo de coluna você usaria? Pesquise sobreVARCHARcomCHECKversus o tipoENUMno PostgreSQL e justifique sua escolha. -
O seed data atual insere eventos com datas futuras. Adicione 3 eventos com datas no passado e status
completed. Verifique que oSELECTretorna todos os 15 eventos comORDER BY date ASC. -
Escreva uma query SQL que retorne apenas os eventos organizados pela Carla, mostrando título, local e data. Use
WHEREcom o nome dela (via JOIN), não com oiddiretamente. -
O script
migrate.tsatual falha se você rodá-lo duas vezes. Modifique as migrations para usarCREATE TABLE IF NOT EXISTSe teste que rodar o script duas vezes funciona sem erros. -
Modifique o
events.repository.tspara quefindAllaceite um parâmetro opcionalorganizerId. Quando presente, a query deve filtrar pororganizer_id. Teste chamandocurlcom um query param (ex:GET /api/events?organizerId=2) — você vai precisar modificar o controller e o service para propagar o parâmetro. -
Adicione um endpoint
GET /api/events/:idque retorna um único evento pelo seuid. 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:
<!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.
<!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.
-
A regra completa de especificidade está documentada em Specificity — MDN. Na prática, usar apenas seletores de classe (
.card,.event-title) e evitar!importantresolve 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:
- Content — o conteúdo em si (texto, imagem).
- Padding — espaço interno entre o conteúdo e a borda.
- Border — a borda visível da caixa.
- 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:
<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:
fetch("/api/events")dispara uma requisição HTTP GET para a API. Oawaitespera a resposta chegar.response.json()lê o corpo da resposta e parseia o JSON, retornando um array de objetos JavaScript.- Para cada evento, criamos elementos HTML com
document.createElement, preenchemos seus conteúdos comtextContente os montamos dentro do card comappend. - 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.
-
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.
-
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
-
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 propriedadestyle.displaypara esconder/mostrar os cards. -
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
toLocaleStringna MDN. -
Adicione um estado “loading” à página: antes do
fetchretornar, mostre o texto “Carregando eventos…” dentro do contêiner. Quando os dados chegarem, substitua pelo conteúdo real. Dica: usecontainer.textContentpara limpar e depoisappendChildpara popular. -
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. Usetry/catchao redor dofetcheresponse.okpara checar se a resposta é 2xx. -
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.togglepara alternar uma classe CSS. -
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:
- HTML: https://developer.mozilla.org/pt-BR/docs/Learn_web_development/Core/Structuring_content
- CSS: https://developer.mozilla.org/pt-BR/docs/Learn_web_development/Core/Styling_basics
- JavaScript e o DOM: https://developer.mozilla.org/pt-BR/docs/Learn_web_development/Core/Scripting
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:
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:
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
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:
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:
loadingcomeça comotrue. Enquanto fortrue, o componente renderiza “Carregando eventos…”.errorcomeça comonull. Se o fetch falhar — rede fora do ar, backend retornando 500 — ocatchsalva a mensagem de erro, e o componente renderiza a mensagem de falha.eventsrecebe 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:
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
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:
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:
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:
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.
-
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:
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:
useStateeuseEffectorganizam 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:
EventListusaEventCard.AppusaEventListeEventDetail. 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
-
Adicione um campo “Criado em” ao
EventDetailque mostre a data de criação do evento formatada comtoLocaleDateString("pt-BR"). Não adicione esse campo aoEventCard— é informação demais para a lista. -
O
EventListatual 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. -
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 rotapath="*"no React Router. -
O
EventDetailbusca a lista inteira e filtra peloid. Se o backend tivesse um endpointGET /api/events/:id(exercício 6 do capítulo Armazenando e Servindo Dados), como o código mudaria? Reescreva ouseEffectpara usar esse endpoint hipotético e compare. -
Implemente um botão “RSVP” no
EventDetail. Ao clicar, ele muda de “RSVP” para “Confirmado ✓” (com cor verde) usandouseState. Esse estado é local — recarregar a página zera o botão. Persistir no servidor é tarefa de um capítulo futuro. -
Adicione um
<input>de busca noEventList. UseuseStatepara armazenar o texto digitado e filtre o arrayeventscom.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:
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:
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.
-
npm workspaces permitem que pacotes dentro de um monorepo se referenciem sem publicar no registry. A configuração envolve um
package.jsonna raiz com"workspaces": ["backend", "web", "mobile", "shared"]e um pacoteshared/com seu própriopackage.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:
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 aokeyno 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
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:
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:
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.
Navegando da lista para o detalhe
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:
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.
-
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:
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
-
Adicione um
ActivityIndicatordentro de cada card enquanto a imagem (futura) do evento carregaria. Por enquanto, simule com umsetTimeoutde 1 segundo nouseEffectantes de setarloadingcomofalse. Observe como o feedback visual muda a percepção de velocidade. -
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
optionscomo função noStack.Screen, que receberoute.params. -
Adicione um
RefreshControlnaFlatListpara 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 doFlatListno React Native. -
O
EventDetailScreenbusca a lista inteira e filtra peloid, assim como a versão web. Uma alternativa é passar o evento inteiro como parâmetro de navegação (emroute.params), evitando um segundo fetch. Implemente essa abordagem e compare: quando ela é suficiente? Quando ela falha? -
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. -
Copie o hook
useEvents.tspara o projeto web e refatore oEventList.tsxpara 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.