1. SSR vs CSR vs Hydration — quem monta o HTML?
Toda página web exibida no navegador é, no fim, uma string de HTML. A diferença entre as estratégias modernas de front-end está em quem produz essa string e quando. As três abordagens dominantes hoje são: Server-Side Rendering (SSR), Client-Side Rendering (CSR) e a abordagem híbrida com hydration. Entender as diferenças não é trivia acadêmica — a escolha define a complexidade da sua build, o tempo de primeiro paint, o comportamento em buscadores e a dificuldade de fazer SEO e acessibilidade.
No Server-Side Rendering, o servidor recebe a requisição, consulta o banco, chama os serviços, monta a string de HTML completa (com os dados já preenchidos) e devolve essa string para o navegador. O navegador exibe a página imediatamente — não há "tela branca" esperando JavaScript. Para uma aplicação simples ou um projeto educacional, SSR é a escolha mais direta: você só precisa de um servidor Node.js, um template engine como EJS, e está pronto. Não há build step, não há bundle gigante, não há node_modules de mil pacotes para fazer um <h1> aparecer.
No Client-Side Rendering, o servidor devolve um HTML quase vazio — geralmente apenas uma <div id="app"></div> e uma referência a um arquivo JavaScript grande. O navegador baixa esse JS, o executa, e só então o JS faz fetch nos dados, monta o DOM dinamicamente e renderiza a interface. Essa é a abordagem clássica de SPAs (Single Page Applications) com React, Vue ou Angular sem framework de SSR. O resultado é uma navegação interna muito rápida (não há reload do navegador) ao custo de um primeiro paint mais lento e SEO problemático.
🖥️ SSR — server monta tudo
- Primeiro paint imediato (HTML pronto)
- SEO nativo — buscadores leem o HTML
- Sem build pesado, sem webpack
- Cada navegação = nova página completa
- Fácil de depurar (View Source mostra o real)
🌐 CSR — client monta tudo
- Primeiro paint depende do JS chegar
- SEO precisa de pre-rendering
- Build complexo (Vite/webpack)
- Navegação interna rápida (sem reload)
- View Source mostra HTML vazio
O hydration é a abordagem híbrida adotada por frameworks como Next.js, Nuxt e Remix. O servidor renderiza o HTML completo (como SSR), mas envia também o JS da aplicação. Quando o JS chega, ele "hidrata" o DOM existente, anexando event listeners e tornando a página interativa como uma SPA. O usuário vê o conteúdo rapidamente e ainda assim tem navegação fluida. O preço é a complexidade: você lida com dois mundos (server e client), com problemas como mismatches de hydration e bundle splitting.
Para a aula 7 e para o projeto integrador do módulo, escolhemos SSR puro com EJS por três razões: (1) nenhum tooling de build é necessário, (2) o aluno enxerga claramente a separação entre server (TypeScript) e client (JS de browser), (3) o resultado é debugável apenas com o "View Source" do navegador. Frameworks com hydration são poderosos, mas escondem o pipeline básico que o aluno precisa entender primeiro.
2. Setup do EJS no Express
Configurar o EJS é uma operação de duas linhas. O Express tem suporte built-in para template engines via app.set('view engine', ...); basta instalar o pacote correspondente e apontar para o diretório de views. A partir desse ponto, qualquer chamada a res.render('caminho/template') dentro de um controller fará o Express procurar views/caminho/template.ejs, executar o template com os dados fornecidos e enviar o HTML resultante como resposta.
O EJS é uma das engines mais simples do ecossistema Node.js. Diferente de Pug (que tem sintaxe própria) ou Handlebars (que limita a lógica permitida no template), o EJS permite JavaScript puro dentro do template. Isso reduz a curva de aprendizado: se você sabe JS, sabe EJS. A contrapartida é que é fácil colocar lógica demais no template — disciplina pessoal e revisão de código resolvem.
import express from 'express';
import path from 'path';
const app = express();
// 1. Diz ao Express qual engine usar para qualquer res.render(...)
app.set('view engine', 'ejs');
// 2. Diretório base dos templates (.ejs)
app.set('views', path.join(__dirname, 'views'));
// 3. Body parser para forms tradicionais (application/x-www-form-urlencoded)
app.use(express.urlencoded({ extended: true }));
// 4. Static files (CSS, JS, imagens em /public)
app.use(express.static('public'));
app.listen(3000);
A instalação é trivial: npm install ejs. O TypeScript não precisa de tipagens adicionais para o setup, pois res.render já é tipado pelo próprio Express. Note que path.join(__dirname, 'views') é uma boa prática — em vez de passar uma string relativa como './views', usar o diretório absoluto evita problemas quando o processo é iniciado a partir de outro diretório (comum em produção, com PM2 ou Docker).
Se você usar TypeScript com módulos ES (ESM, com "type": "module" no package.json), __dirname não existe. Nesse caso, derive-o de import.meta.url: const __dirname = path.dirname(fileURLToPath(import.meta.url));. Para o módulo 2, mantemos CommonJS (sem ESM) e o __dirname funciona naturalmente.
3. Estrutura de views/ e partials
Templates EJS, assim como qualquer estrutura de código, beneficiam-se de uma organização clara. A convenção que usaremos no projeto integrador segue dois princípios: organização por recurso (cada entidade do domínio tem sua própria pasta) e composição via partials (header, footer, navegação são reusados em todas as páginas). Isso evita o problema clássico de copiar e colar o mesmo <header> em vinte arquivos e ter que atualizar todos quando o logo muda.
src/views/
├── layouts/
│ └── main.ejs # shell HTML <html>...</html>
├── partials/
│ ├── header.ejs # <header> + nav
│ ├── footer.ejs # <footer>
│ └── flash.ejs # mensagens de sucesso/erro
├── home.ejs # GET /
├── users/
│ ├── index.ejs # GET /users
│ ├── show.ejs # GET /users/:id
│ ├── new.ejs # GET /users/new (form)
│ └── edit.ejs # GET /users/:id/edit (form)
└── errors/
├── 404.ejs
└── 500.ejs
Os partials são pedaços de HTML pequenos e reusáveis. Tipicamente: o cabeçalho com logo e menu, o rodapé com créditos, blocos de mensagens flash, a sidebar de filtros. Você os inclui em qualquer template com a tag <%- include('caminho/relativo') %>. O caminho é relativo ao próprio template que faz o include — não ao diretório views/.
<%- include('../partials/header') %>
<main>
<h1>Usuários</h1>
<ul>
<% users.forEach(u => { %>
<li><%= u.nome %></li>
<% }) %>
</ul>
</main>
<%- include('../partials/footer') %>
O EJS puro não tem o conceito de "layout" como o Pug ou o Handlebars têm — você precisa replicar o <html>...</html> em cada template ou usar partials para o cabeçalho e rodapé. Para projetos pequenos isso é aceitável; para projetos maiores, o pacote express-ejs-layouts resolve, permitindo declarar um layout único e cada view só fornecer o conteúdo do <main>.
4. Sintaxe EJS — as 4 tags principais
A sintaxe do EJS é minimalista. São, na prática, quatro tags que cobrem 95% dos casos. Todas elas são delimitadas por <% e %> — qualquer coisa fora desses delimitadores é HTML cru, copiado para a saída sem modificação. Dentro deles, JavaScript puro.
<, >, &, ', "). Esta é a tag padrão para qualquer conteúdo de variável. Defesa primária contra XSS.if, for, forEach, declarações de variáveis. A combinação com <% } %> fecha estruturas.include('partials/card', { title: 'Hi' }) — variáveis adicionais visíveis dentro do partial.Exemplo combinando todas as tags
<%- include('partials/header', { title: 'Usuários' }) %>
<main>
<h1>Olá, <%= currentUser.nome %>!</h1>
<% if (users.length === 0) { %>
<p>Nenhum usuário cadastrado.</p>
<% } else { %>
<ul>
<% users.forEach(u => { %>
<li>
<a href="/users/<%= u.id %>">
<%= u.nome %> — <%= u.email %>
</a>
</li>
<% }) %>
</ul>
<% } %>
</main>
<%- include('partials/footer') %>
Note como cada tag tem um papel distinto: o include traz partials, o <%= %> imprime variáveis com escape, o <% if/forEach %> faz o controle de fluxo. Esse é o padrão que se repete em todos os seus templates.
| Tag | Imprime? | Escapa HTML? | Quando usar |
|---|---|---|---|
<%= var %> | Sim | Sim | Conteúdo do usuário, valores de banco |
<%- var %> | Sim | Não | HTML pré-renderizado, includes |
<% código %> | Não | — | if/forEach/variáveis |
<%- include(...) %> | Sim | Não | Composição via partials |
5. Controller chamando res.render
O ponto onde a arquitetura MVC encontra o template engine é a chamada a res.render dentro do controller. O controller é responsável por orquestrar: receber a requisição HTTP, delegar a lógica de negócio ao service (que por sua vez chama o repository), receber os dados, e escolher qual template renderizar com quais dados. O template engine assume daí em diante e produz o HTML.
A assinatura básica é res.render(templatePath, data). O templatePath é relativo ao diretório de views (configurado no setup) e sem extensão — 'users/index' resolverá para views/users/index.ejs. O segundo argumento é um objeto cujas chaves se tornam variáveis disponíveis dentro do template.
import { Request, Response } from 'express';
import { UsersService } from '../services/users.service';
export class UsersController {
constructor(private readonly service: UsersService) {}
// GET /users — lista todos
async index(req: Request, res: Response) {
const users = await this.service.list();
const currentUser = req.session?.user ?? null;
res.render('users/index', { users, currentUser });
}
// GET /users/:id — detalhe
async show(req: Request, res: Response) {
const user = await this.service.findById(req.params.id);
if (!user) return res.status(404).render('errors/404');
res.render('users/show', { user });
}
}
Repare em três detalhes importantes do código acima. Primeiro, o controller é uma classe TypeScript com injeção de dependência no construtor — ele recebe o service em vez de criar um internamente. Esse padrão facilita testes unitários (você passa um mock do service). Segundo, o controller não conhece SQL — ele só conversa com o service, que por sua vez conversa com o repository. Terceiro, o controller decide o status HTTP e o template; o EJS apenas renderiza com os dados que recebe.
Fluxo end-to-end de uma requisição GET /users
- O navegador faz
GET /users. - O Express casa a rota com
UsersController.index. - O controller chama
service.list(). - O service aplica regras de negócio e chama
repository.findAll(). - O repository executa a query SQL via
pge retornaUser[]. - O controller faz
res.render('users/index', { users }). - O EJS abre
views/users/index.ejs, executa o template com a variávelusers, gera a string HTML. - O Express envia o HTML como resposta com status 200.
- O navegador parseia o HTML e exibe a página.
Se uma variável precisa estar disponível em todos os templates (ex.: o usuário logado, o ano corrente para o footer, configs do site), use res.locals em um middleware. Qualquer chave em res.locals fica acessível no EJS sem precisar passá-la em cada render.
6. HTML semântico
HTML semântico é o uso de tags que descrevem o significado do conteúdo, não a sua aparência. Em vez de envolver tudo em <div> com classes (a famosa "div soup"), você usa <header> para o cabeçalho, <nav> para a navegação, <main> para o conteúdo principal, <article> para uma unidade autônoma de conteúdo, <section> para agrupamentos temáticos, <aside> para conteúdo lateral e <footer> para o rodapé. Visualmente, um <header> é igual a uma <div> — a diferença está no significado que essa tag carrega.
Esse significado importa para três audiências distintas. Leitores de tela usados por pessoas com deficiência visual constroem uma "lista de marcos" da página com base nessas tags — o usuário pode pular direto para o <main> ou para o <nav> com um atalho. Sem semântica, esse atalho não funciona, e a pessoa precisa percorrer linearmente todo o conteúdo. Buscadores como Google extraem hierarquia de informação para o ranking — uma página com <article> bem estruturado tende a aparecer melhor em SERP. Outros desenvolvedores entendem mais rápido um código onde as tags se autoexplicam — <footer> é claramente o rodapé, sem precisar olhar a classe CSS.
| Tag | Significado | Quando usar |
|---|---|---|
<header> | Cabeçalho da página ou de uma seção | Logo, título do site, breadcrumb |
<nav> | Bloco de navegação principal | Menu superior, sidebar de links |
<main> | Conteúdo principal único da página | Aparece UMA vez por página |
<section> | Agrupamento temático com título | Capítulos, abas, blocos relacionados |
<article> | Unidade autônoma de conteúdo | Post de blog, card de produto |
<aside> | Conteúdo lateral, complementar | Sidebar, propaganda, "veja também" |
<footer> | Rodapé da página ou de uma seção | Créditos, links secundários |
Imagens devem ter alt descritivo. Inputs devem ter <label for="id">. Botões clicáveis devem ser <button> (não <div onclick>). Ícones decorativos devem ter aria-hidden="true". Cores nunca devem ser o único meio de transmitir informação. Esses são os 5 itens que mais retornam em auditorias de acessibilidade.
7. Forms tradicionais — POST direto para o servidor
Antes de qualquer fetch ou AJAX, o jeito original e ainda perfeitamente válido de enviar dados para o servidor é o form HTML tradicional. O <form method="POST" action="/users"> faz o navegador enviar uma requisição POST para a URL indicada, com os campos do formulário codificados como application/x-www-form-urlencoded. Após a resposta, o navegador recarrega a página com o novo HTML. É simples, funciona sem JavaScript e cobre 80% dos casos de aplicações CRUD.
<form action="/users" method="POST">
<label for="nome">Nome</label>
<input id="nome" name="nome" type="text" required maxlength="80">
<label for="email">E-mail</label>
<input id="email" name="email" type="email" required>
<button type="submit">Cadastrar</button>
</form>
No servidor, esse form chega no controller via req.body — desde que o middleware express.urlencoded({ extended: true }) esteja ativo (vimos isso no setup). O controller valida, chama o service e responde geralmente com um res.redirect('/users') para o padrão "Post-Redirect-Get": o usuário é redirecionado após o POST para evitar reenvio do form ao recarregar a página.
// POST /users
async create(req: Request, res: Response) {
const { nome, email } = req.body; // vem do form urlencoded
await this.service.create({ nome, email });
res.redirect('/users'); // Post-Redirect-Get
}
Forms tradicionais não substituem fetch (que veremos na aula 8). São complementares: forms são ótimos para create/update simples e quando JavaScript pode falhar; fetch é melhor para experiências sem reload, listas com filtros dinâmicos e UIs mais ricas.
8. Static files — CSS, JS e imagens
Templates EJS produzem o HTML. Mas o HTML referencia arquivos externos: folhas de estilo CSS, scripts JavaScript de cliente e imagens. Esses arquivos precisam ser servidos pelo Express, e o middleware express.static resolve isso em uma linha: app.use(express.static('public')). A partir desse ponto, qualquer arquivo dentro de public/ é acessível por uma URL relativa.
public/
├── css/
│ └── site.css # acessível em /css/site.css
├── js/
│ └── app.js # acessível em /js/app.js
└── img/
└── logo.png # acessível em /img/logo.png
<head>
<link rel="stylesheet" href="/css/site.css">
</head>
<body>
<img src="/img/logo.png" alt="Logo">
<script src="/js/app.js"></script>
</body>
O nome do diretório (public) não aparece na URL. public/css/site.css é servido em /css/site.css, não em /public/css/site.css. Isso é uma das poucas pegadinhas comuns — alunos que não percebem isso ficam vendo 404 e ficam confusos.
Tudo dentro de public/ é exposto publicamente. Nunca coloque ali .env, configurações com credenciais, dumps de banco ou backups. O Express vai entregá-los para qualquer um que adivinhar o caminho.
9. DOM essencial — JavaScript no navegador
O HTML que chega no navegador é parseado e transformado em uma árvore de objetos: o DOM (Document Object Model). Cada tag vira um nó dessa árvore, com propriedades, métodos e relações pai/filho. O JavaScript de cliente — aquele rodando no <script> da página — manipula essa árvore para mudar conteúdo, alterar estilos, adicionar elementos, escutar eventos do usuário.
O DOM moderno é poderoso e a maioria das tarefas comuns é resolvida com três APIs apenas: document.querySelector para selecionar elementos, element.addEventListener para escutar eventos, e element.classList para manipular classes CSS. Antes do navegador ter essas APIs (era do jQuery), todas essas operações eram verbosas e inconsistentes entre browsers. Hoje, JavaScript de cliente é tão expressivo quanto qualquer biblioteca.
// 1. Selecionar elementos com seletores CSS
const btn = document.querySelector('#menu-btn');
const menu = document.querySelector('.menu');
// 2. Escutar eventos
btn.addEventListener('click', (e) => {
// 3. Manipular classes
menu.classList.toggle('open');
btn.setAttribute('aria-expanded', menu.classList.contains('open'));
});
// querySelectorAll retorna NodeList — itere com forEach
document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
});
});
| API | O que faz | Exemplo |
|---|---|---|
querySelector(sel) | Primeiro elemento que casa o seletor CSS | document.querySelector('#btn') |
querySelectorAll(sel) | NodeList com todos os elementos | document.querySelectorAll('.card') |
addEventListener(ev, fn) | Anexa handler a um evento | btn.addEventListener('click', fn) |
classList.add/remove/toggle | Manipula classes CSS | menu.classList.toggle('open') |
textContent | Conteúdo de texto puro (seguro) | span.textContent = 'Olá' |
createElement / appendChild | Cria e insere novos nós | list.appendChild(li) |
10. Eventos no DOM — bubbling e preventDefault
Eventos são a forma como o navegador comunica ao seu JS que algo aconteceu: o usuário clicou, digitou, redimensionou a janela, terminou de carregar uma imagem. O método addEventListener conecta uma função ("handler") ao evento de um elemento. Quando o evento dispara, o handler é chamado com um objeto Event contendo metadados.
Dois conceitos fundamentais: capture vs bubbling e preventDefault. Quando um clique acontece em um botão dentro de uma seção dentro de um main, o navegador dispara o evento em três fases — primeiro descendo pela árvore (capture), depois no alvo (target), depois subindo pela árvore (bubbling). Por padrão, addEventListener escuta na fase de bubbling. Você só vai mexer nisso em casos específicos (ex.: implementar um modal que fecha ao clicar fora). Para 90% dos casos, o comportamento padrão funciona.
O método event.preventDefault() cancela a ação default do navegador para aquele evento. Em um <a href="...">, ele impede a navegação. Em um <form>, ele impede o submit (e o reload da página). Esse método é fundamental para validação no cliente: você intercepta o submit, valida os campos via JS, e só deixa o form prosseguir se tudo estiver correto.
const form = document.querySelector('#user-form');
const emailInput = document.querySelector('#email');
const emailError = document.querySelector('#email-error');
form.addEventListener('submit', (e) => {
const email = emailInput.value.trim();
if (!email.includes('@')) {
e.preventDefault(); // trava o submit
emailError.textContent = 'E-mail inválido';
emailInput.focus();
}
});
click e submit são os mais usados. Outros que aparecem com frequência: input (digitação em real-time), change (após perder foco), focus/blur (entrar/sair do campo), keydown (atalhos de teclado), DOMContentLoaded (HTML pronto, antes das imagens).
11. Passando dados do servidor para o script de cliente
Em SSR puro, há momentos em que o JS de cliente precisa de dados que o servidor já tem em mão. Você poderia fazer um fetch separado (e veremos isso na aula 8), mas para dados pequenos e que já chegaram com a renderização inicial, a estratégia mais eficiente é injetar os dados em uma variável global JavaScript dentro do próprio template EJS. Isso evita uma round-trip extra ao servidor.
<ul id="users-list">
<% users.forEach(u => { %>
<li data-id="<%= u.id %>"><%= u.nome %></li>
<% }) %>
</ul>
<script>
// JSON.stringify gera JS válido — usamos <%- (raw) para não escapar as aspas
window.USERS = <%- JSON.stringify(users) %>;
</script>
<script src="/js/app.js"></script>
Repare em três pontos. Primeiro, a tag <%- JSON.stringify(...) %> usa <%- (raw) e não <%= (escapado), porque queremos a string JSON literal — não escapada — chegar no JavaScript. Segundo, a variável global é colocada em window explicitamente, deixando claro que é global. Terceiro, o script externo (/js/app.js) deve vir depois do script inline, para que window.USERS já exista quando ele executar.
Outra opção, especialmente útil quando os dados são pequenos e ligados a um elemento específico, é usar data attributes: <li data-id="42" data-status="ativo">. No JS, leia com el.dataset.id. Essa abordagem é elegante para listas onde cada item carrega seus próprios metadados.
Tudo que você imprime no template chega no HTML — view source mostra. Nunca injete tokens, hashes de senha ou dados que não devem ser visíveis ao usuário. Use o que está em req.session com cuidado: filtre o objeto antes de passar para o EJS.
12. Validação dupla — cliente para UX, servidor para segurança
Toda aplicação web séria tem validação em duas camadas: no navegador e no servidor. Elas servem propósitos diferentes e nenhuma substitui a outra. A validação no cliente existe para UX — feedback rápido, antes mesmo de fazer a requisição. A validação no servidor existe para segurança e integridade — é a única que tem valor real, porque a do cliente pode ser desabilitada com 3 cliques no DevTools.
No cliente, a primeira linha de defesa são os atributos HTML5 nativos: required, min/max, minlength/maxlength, pattern, type="email", type="number". O navegador já faz a validação básica e mostra mensagens de erro. Para validação mais sofisticada (cross-field, regras de negócio), use JS com preventDefault() no handler de submit.
<input
name="email"
type="email"
required
maxlength="120"
pattern="^\S+@\S+\.\S+$"
>
<input
name="idade"
type="number"
min="0"
max="120"
required
>
No servidor, vimos na aula 6 o uso de express-validator e middlewares próprios para validar req.body antes de chegar no controller. Isso permanece obrigatório mesmo quando o cliente já validou. Um atacante pode enviar uma requisição direto via curl, ignorando todo o seu HTML. Tratar o cliente como adversário é a postura correta — não paranoia, e sim defesa em profundidade.
| Camada | Quando roda | Pode ser burlada? | Propósito |
|---|---|---|---|
| Cliente (HTML5 + JS) | Antes do submit | SIM (DevTools) | UX — feedback rápido |
| Servidor (express-validator) | Antes do controller | NÃO | Segurança e integridade |
13. Segurança · XSS e o uso correto de <%= %>
XSS (Cross-Site Scripting) é uma das vulnerabilidades mais comuns da web. Acontece quando um atacante consegue injetar JavaScript malicioso na página exibida para outro usuário. Em uma aplicação SSR com EJS, o vetor clássico é o conteúdo do usuário renderizado sem escape. Imagine um campo de comentário onde o atacante envia <script>alert(document.cookie)</script> — se o template imprime isso com <%- %> (raw), o script roda no navegador da próxima vítima.
A defesa primária é simples: use sempre <%= %> para conteúdo vindo de usuário ou banco. Esta tag escapa caracteres HTML especiais — < vira <, > vira >, etc. O navegador exibe os caracteres mas não os interpreta como tags. O <%- %> deve ser reservado para situações em que você sabe que o conteúdo é seguro: includes de partials, HTML pré-renderizado por outra parte do servidor que você controla.
// SEGURO: o <script> vira <script> no HTML
<p><%= comentario.texto %></p>
// PERIGOSO: se texto = "<script>alert('xss')</script>",
// o script EXECUTA no navegador da vítima.
<p><%- comentario.texto %></p>
Outras camadas de defesa contra XSS — todas válidas e complementares:
- Sanitização no servidor: bibliotecas como
DOMPurifyousanitize-htmlremovem tags perigosas antes de salvar/exibir. - Content Security Policy (CSP): header HTTP que diz ao navegador quais scripts são permitidos. Bloqueia
<script>inline desconhecidos mesmo se eles passarem. - HttpOnly cookies: cookies com flag HttpOnly não são acessíveis via
document.cookie, frustrando o roubo de sessão. - Validação rígida: rejeitar input com tags HTML quando o campo deveria ser texto puro.
Em dúvida, use <%= %>. O EJS por design favorece a tag escapada — só desça para <%- %> quando você tiver certeza absoluta de que o conteúdo é seguro. "Acho que é seguro" não é certeza absoluta.
14. Helpers EJS via app.locals e res.locals
A 6ª camada da arquitetura do projeto — Helpers — comporta funções utilitárias reusadas em vários lugares. Algumas dessas funções são úteis dentro dos templates: formatar datas, formatar moeda, pluralizar palavras, gerar URLs canônicas. EJS aceita funções como variáveis: se você passar uma função no objeto de res.render, ela fica disponível para chamada no template.
Quando uma função é usada em todos os templates, é uma má prática passá-la em cada controller. A solução é app.locals (variáveis globais para todas as requisições) ou res.locals (variáveis para a requisição atual). Tudo que está nesses objetos fica acessível no EJS sem precisar passar pelo render.
// src/helpers/format.ts
export const formatCurrency = (cents: number) =>
(cents / 100).toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' });
export const formatDate = (d: Date) =>
Intl.DateTimeFormat('pt-BR').format(d);
// src/app.ts — registra UMA vez ao iniciar
import { formatCurrency, formatDate } from './helpers/format';
app.locals.formatCurrency = formatCurrency;
app.locals.formatDate = formatDate;
<td><%= formatCurrency(produto.preco_centavos) %></td>
<td><%= formatDate(pedido.criado_em) %></td>
A diferença prática entre app.locals e res.locals: o primeiro é setado uma única vez ao iniciar o servidor e vale para todas as requisições; o segundo é setado dentro de um middleware (típico para o usuário logado, flash messages, configs por requisição) e vale só para aquela requisição. O EJS vê os dois como variáveis disponíveis no template — a fonte é transparente.
15. Desafio prático — Sua Tela do Projeto em EJS
Antes de consolidar o trabalho em equipe, cada aluno precisa entregar uma tela do wireframe de alta fidelidade do projeto renderizada via SSR + EJS. O exercício é individual e tem dois objetivos: garantir que cada integrante materializou sozinho a stack de templates (sintaxe, partials, semântica) e adiantar uma tela real do produto, em vez de exercício descartável. Cada aluno fica responsável por uma tela diferente do wireframe — a divisão é combinada na daily da manhã.
O desafio é executado durante a primeira meia hora do bloco de desenvolvimento (14h00 — 14h40 no plano operacional). A entrega é obrigatória e pessoal — cada aluno faz e entrega a própria tela.
Requisito principal
Escolher uma das telas já desenhadas no wireframe de alta fidelidade do projeto e replicá-la em EJS via uma rota GET dedicada (ex.: res.render('produtos/index', { produtos })). Os dados precisam vir do controller (objeto ou array mockado), nunca hardcoded direto no template.
- 🎨 Tela do wireframe escolhida na daily — listagem, detalhe ou formulário. Sem inventar tela nova.
- 📐 Fidelidade visual ao wireframe: hierarquia, blocos, ordem dos elementos e conteúdo aproximado. Pixel-perfect fica para a Aula 10 (CSS).
- 🔁 Iteração com EJS onde o wireframe tem listas ou cards repetidos:
<% items.forEach(i => { %>. - 🧩 Reuso via
includepara os blocos que aparecem em outras telas (header, nav, footer, cards).
Restrições técnicas
- Controller com os dados da tela (objeto ou array mockado). Sem dados hardcoded no template — o template apenas itera e exibe.
- Layout base em
views/layouts/main.ejsdefinindo a estrutura HTML do documento. - Partials obrigatórios:
views/partials/header.ejseviews/partials/footer.ejs, importados via<%- include('partials/header') %>. - HTML semântico: usar
<header>,<main>,<section>,<article>conforme a hierarquia do conteúdo. Reprovar a "soup of divs". - CSS em
public/css/servido porexpress.static('public')e referenciado por caminho absoluto (/css/...). - Sem framework de front (nada de React/Vue/Svelte) e sem
fetch— só SSR puro.fetchchega na Aula 8.
Exemplo de estrutura mínima do controller
export class ProdutosController {
index(req: Request, res: Response) {
const produtos = [
{ id: 1, nome: 'Camiseta Inteli', preco: 79.9, categoria: 'Vestuário' },
{ id: 2, nome: 'Caneca Inteli', preco: 39.0, categoria: 'Acessórios' },
{ id: 3, nome: 'Adesivo Pack', preco: 12.5, categoria: 'Acessórios' },
];
res.render('produtos/index', { produtos, tela: 'Catálogo' });
}
}
Bônus opcionais
Não são exigidos para fechar a entrega, mas exercitam tópicos da aula:
- Toggle dark mode com
classList.toggle('dark')num botão doheader.ejs. - Route param real (ex.:
/produtos/:id) buscando no array mockado e renderizando uma segunda view de detalhe. - Botão "Imprimir" usando
window.print()direto no template. - Filtro/busca client-side em JS vanilla sobre os itens já renderizados (sem fetch, sem reload).
Entrega
- Branch
aula-7-tela-projetono repositório do seu projeto integrador. - Print da página renderizada lado a lado com o wireframe de origem no canal da turma.
- Prazo: até as 14h40. Depois disso, a equipe integra as telas individuais e fecha a entrega em grupo.
A entrega vale por conformidade, não por estética. Os pontos verificados são: dados no controller (não no template), layout + partials no lugar correto, tags semânticas usadas com sentido, fidelidade estrutural ao wireframe e CSS via static. Estilo pixel-perfect é bônus e fica para a aula de CSS.
16. Checklist de Estudo
Clique nos itens à medida que dominar cada conceito:
- ✓Consigo explicar a diferença entre SSR, CSR e hydration com um exemplo concreto.
- ✓Configuro o EJS no Express com
app.set('view engine', 'ejs')eapp.set('views', ...). - ✓Organizo
views/com layouts, partials e pastas por recurso. - ✓Sei a diferença entre
<%= %>,<%- %>e<% %>e quando usar cada um. - ✓Uso
<%- include('partials/...') %>para compor templates. - ✓No controller, chamo
res.render('caminho', { dados })seguindo o fluxo MVC. - ✓Escrevo HTML com tags semânticas (
header,nav,main,article,footer). - ✓Configuro
express.static('public')e referencio CSS/JS por caminhos absolutos. - ✓Crio um form HTML que faz POST e processo o body com
express.urlencoded. - ✓Uso
querySelector,addEventListenereclassList.toggleno JS de cliente. - ✓Uso
event.preventDefault()em submits para validação client-side. - ✓Sei passar dados do servidor para o JS via
<script>window.X = <%- JSON.stringify(...) %></script>. - ✓Entendo por que validação no cliente NUNCA substitui validação no servidor.
- ✓Sei reconhecer um vetor de XSS e por que
<%= %>é a defesa primária. - ✓Registro helpers globais em
app.localse os uso dentro do EJS. - ✓Entreguei o desafio Tela do Projeto em EJS na branch
aula-7-tela-projetoreplicando uma tela do wireframe com controller, layout, partials e HTML semântico.
Referências
- EJS · Documentação Oficial — sintaxe, includes, options e API completa
- Express · Using Template Engines — guia oficial sobre
res.rendere setup de engines - Express · Serving Static Files — middleware
express.static - MDN · HTML Element Reference — referência completa de tags HTML, semântica e atributos
- MDN · Introduction to the DOM — visão geral da árvore DOM e APIs principais
- MDN · addEventListener — assinaturas, fases capture/bubbling e options
- OWASP · Cross-Site Scripting (XSS) — fundamentos da vulnerabilidade e como evitar
- web.dev · Rendering on the Web — artigo do Google sobre SSR, CSR, hydration e static rendering