JavaScript Fullstack
do zero ao mercado de trabalho
Um guia progressivo em 18 módulos: fundamentos, frontend, backend com Node.js, três projetos completos para o portfólio e preparação para entrevistas técnicas. Cada módulo tem exercícios com solução e dicas do que as empresas realmente cobram.
Siga os módulos em ordem — cada um assume o anterior. Digite todo código você mesmo (não copie e cole): é assim que a sintaxe entra na memória. Faça os exercícios antes de abrir a solução. Marque a caixa "concluído" no fim de cada módulo para acompanhar seu progresso (fica salvo no seu navegador).
Tempo estimado: 10 a 14 semanas estudando ~1h30 por dia. É um ritmo realista para chegar ao nível júnior.
01 Primeiros passos
Objetivo: entender o que é JavaScript, onde ele roda e escrever seu primeiro código hoje mesmo.
O que é JavaScript?
JavaScript (JS) é a linguagem de programação da web. Ela nasceu em 1995 para dar interatividade a páginas, mas hoje roda em praticamente qualquer lugar: no navegador (frontend), em servidores (backend, via Node.js), em aplicativos de celular e desktop. É por isso que um desenvolvedor fullstack JavaScript é tão procurado: com uma única linguagem você constrói o sistema inteiro.
Três termos que você verá o tempo todo:
- HTML — a estrutura da página (títulos, botões, textos).
- CSS — a aparência (cores, tamanhos, posições).
- JavaScript — o comportamento (o que acontece quando você clica, digita, carrega dados).
Preparando o ambiente
- Instale o VS Code (editor de código gratuito):
code.visualstudio.com. - Instale o Node.js (versão LTS):
nodejs.org. Ele permite rodar JS fora do navegador — essencial para o backend. - Use um navegador moderno (Chrome, Edge ou Firefox) e aprenda a abrir o DevTools com
F12. A aba Console é seu laboratório.
Seu primeiro código
Abra o DevTools (F12 → Console) e digite:
console.log("Olá, mundo!");
// Olá, mundo!
O console.log() imprime valores no console. Você vai usá-lo milhares de vezes para inspecionar o que seu código está fazendo.
Rodando JS em um arquivo HTML
index.html<!DOCTYPE html>
<html lang="pt-BR">
<body>
<h1>Minha primeira página</h1>
<script src="app.js"></script>
</body>
</html>
app.js
alert("Bem-vindo(a)!");
console.log("A página carregou.");
Rodando JS com Node.js
# no terminal, dentro da pasta do arquivo:
node app.js
JavaScript aparece há mais de dez anos entre as linguagens mais usadas do mundo nas pesquisas da Stack Overflow. Nas vagas brasileiras, os termos que mais acompanham JS são: React, Node.js, TypeScript, Git e APIs REST — exatamente o caminho desta apostila.
1.1 — No console do navegador, use console.log para imprimir seu nome, sua idade e a frase "Estou aprendendo JavaScript".
1.2 — Crie uma pasta estudos-js, dentro dela um arquivo ola.js com um console.log, e rode com node ola.js.
▸ Ver solução
// 1.1 (no console do navegador)
console.log("Maria");
console.log(25);
console.log("Estou aprendendo JavaScript");
// 1.2 — conteúdo de ola.js:
console.log("Rodando JS com Node!");
// terminal: node ola.js
02 Variáveis e tipos de dados
Objetivo: guardar informações na memória e entender os tipos de dados que o JS oferece.
Declarando variáveis: let e const
let idade = 25; // pode mudar depois
const nome = "Ana"; // nunca muda (constante)
idade = 26; // ok
nome = "Bia"; // ❌ TypeError: Assignment to constant variable
Regra profissional: use const por padrão; troque para let só quando o valor realmente precisar mudar. Isso deixa o código mais previsível. Existe também o antigo var — evite-o (você entenderá o motivo no módulo 10).
Tipos primitivos
| Tipo | Exemplo | Para quê |
|---|---|---|
string | "olá", 'oi', `texto` | Textos |
number | 42, 3.14, -7 | Números (inteiros e decimais são o mesmo tipo) |
boolean | true, false | Verdadeiro/falso — base de toda decisão |
undefined | let x; | Variável declarada sem valor |
null | let y = null; | Ausência intencional de valor |
bigint | 9007199254740993n | Inteiros gigantes |
symbol | Symbol("id") | Identificadores únicos (uso avançado) |
Além dos primitivos, existe o tipo object — que inclui objetos, arrays e funções (módulos 4 e 5).
Descobrindo o tipo: typeof
typeof "oi" // "string"
typeof 10 // "number"
typeof true // "boolean"
typeof undefined // "undefined"
typeof null // "object" ← bug histórico do JS! Cai em entrevista.
Strings na prática
const nome = "Carlos";
const msg = `Olá, ${nome}! Você tem ${2026 - 1998} anos.`;
// template literal: crase + ${expressão}. Use sempre para montar textos.
nome.length // 6
nome.toUpperCase() // "CARLOS"
nome.includes("ar") // true
nome[0] // "C"
Números e conversões
Number("42") // 42 (string → número)
String(42) // "42" (número → string)
parseFloat("3.9kg") // 3.9
0.1 + 0.2 // 0.30000000000000004 ← ponto flutuante; clássico de entrevista
(0.1 + 0.2).toFixed(2) // "0.30"
"Qual a diferença entre null e undefined?" — undefined significa que a variável existe mas nunca recebeu valor (é o "padrão" do JS). null é um valor atribuído de propósito pelo programador para dizer "vazio intencionalmente". Bônus: typeof null retorna "object" por um bug histórico mantido por compatibilidade.
2.1 — Crie constantes nome, cidade e anoNascimento. Monte com template literal: "Meu nome é X, moro em Y e tenho Z anos".
2.2 — O que imprime typeof typeof 42? Pense antes de testar.
2.3 — Converta a string "R$ 19.90" no número 19.9.
▸ Ver solução
// 2.1
const nome = "Ana", cidade = "Recife", anoNascimento = 2000;
const idade = new Date().getFullYear() - anoNascimento;
console.log(`Meu nome é ${nome}, moro em ${cidade} e tenho ${idade} anos`);
// 2.2 → "string", pois typeof 42 é a string "number",
// e typeof de uma string é "string".
// 2.3
const preco = parseFloat("R$ 19.90".replace("R$ ", "")); // 19.9
03 Operadores e controle de fluxo
Objetivo: fazer o programa tomar decisões e repetir tarefas — a essência da lógica de programação aplicada ao JS.
Operadores essenciais
// Aritméticos
10 + 3 // 13
10 % 3 // 1 (resto da divisão — muito usado: par/ímpar, ciclos)
2 ** 10 // 1024 (potência)
// Comparação — USE SEMPRE === e !==
5 === "5" // false (compara valor E tipo)
5 == "5" // true (converte tipos antes — fonte de bugs, evite)
// Lógicos
a && b // E: os dois verdadeiros
a || b // OU: pelo menos um verdadeiro
!a // NÃO: inverte
Em contextos booleanos, apenas estes valores são considerados falsos: false, 0, "" (string vazia), null, undefined e NaN. Todo o resto é truthy — inclusive "0", [] e {}. Isso cai direto em entrevistas e causa bugs reais.
Condicionais
const nota = 7.5;
if (nota >= 7) {
console.log("Aprovado");
} else if (nota >= 5) {
console.log("Recuperação");
} else {
console.log("Reprovado");
}
// Operador ternário — para escolhas curtas
const status = nota >= 7 ? "Aprovado" : "Reprovado";
// switch — quando há muitos casos fixos
switch (diaSemana) {
case "sab":
case "dom": console.log("Fim de semana"); break;
default: console.log("Dia útil");
}
Laços (loops)
// for clássico — quando você precisa do índice
for (let i = 0; i < 5; i++) {
console.log(`Volta ${i}`);
}
// for...of — percorre valores de um array (o mais usado no dia a dia)
const frutas = ["maçã", "uva", "kiwi"];
for (const fruta of frutas) {
console.log(fruta);
}
// while — repete enquanto a condição for verdadeira
let tentativas = 3;
while (tentativas > 0) {
tentativas--;
}
// break interrompe o laço; continue pula para a próxima volta
"Qual a diferença entre == e ===?" — == faz coerção de tipos antes de comparar ("5" == 5 é true); === compara valor e tipo sem converter. Em código profissional usa-se === praticamente sempre, e ferramentas de lint (ESLint) reforçam isso.
3.1 — FizzBuzz (clássico absoluto de entrevista): imprima de 1 a 100; múltiplos de 3 viram "Fizz", múltiplos de 5 viram "Buzz", múltiplos de ambos viram "FizzBuzz".
3.2 — Some todos os números pares de 1 a 1000.
3.3 — Dado const senha = "js2026", simule 3 tentativas de login com while comparando com valores de um array de tentativas.
▸ Ver solução
// 3.1 — FizzBuzz
for (let i = 1; i <= 100; i++) {
if (i % 15 === 0) console.log("FizzBuzz");
else if (i % 3 === 0) console.log("Fizz");
else if (i % 5 === 0) console.log("Buzz");
else console.log(i);
}
// 3.2
let soma = 0;
for (let i = 2; i <= 1000; i += 2) soma += i;
console.log(soma); // 250500
// 3.3
const senha = "js2026";
const tentativas = ["123", "abc", "js2026"];
let i = 0;
while (i < tentativas.length) {
if (tentativas[i] === senha) { console.log("Acesso liberado"); break; }
console.log("Senha incorreta");
i++;
}
04 Funções
Objetivo: organizar código em blocos reutilizáveis. Funções são o coração do JavaScript — literalmente tudo no ecossistema gira em torno delas.
Três formas de criar funções
// 1. Declaração de função (function declaration)
function somar(a, b) {
return a + b;
}
// 2. Expressão de função (function expression)
const subtrair = function (a, b) {
return a - b;
};
// 3. Arrow function (ES6) — a mais usada hoje
const multiplicar = (a, b) => a * b; // return implícito em uma linha
const dobro = n => n * 2; // 1 parâmetro dispensa parênteses
const saudacao = () => { // corpo com chaves exige return
return "Olá!";
};
Parâmetros: padrão, rest e retorno
// Valor padrão
function saudar(nome = "visitante") {
return `Olá, ${nome}!`;
}
saudar(); // "Olá, visitante!"
// Rest: agrupa argumentos em um array
const somarTudo = (...numeros) =>
numeros.reduce((total, n) => total + n, 0);
somarTudo(1, 2, 3, 4); // 10
// Sem return explícito, a função devolve undefined
Funções como valores (cidadãs de primeira classe)
Em JS, função é um valor como qualquer outro: pode ser guardada em variável, passada como argumento e retornada por outra função. Uma função passada como argumento é chamada de callback.
function executar(operacao, a, b) {
return operacao(a, b); // chama o callback
}
executar((x, y) => x * y, 4, 5); // 20
// Você já usa callbacks sem perceber:
setTimeout(() => console.log("2 segundos depois"), 2000);
Escopo: onde a variável "enxerga"
const global = "todos veem";
function exemplo() {
const local = "só dentro da função";
if (true) {
const bloco = "só dentro do if"; // let/const têm escopo de BLOCO
}
// bloco não existe aqui
}
// local não existe aqui
Funções pequenas com um único propósito, nomes que são verbos (calcularTotal, buscarUsuario), no máximo 2–3 parâmetros (acima disso, receba um objeto). Código assim é o que times avaliam em desafios técnicos.
4.1 — Escreva ehPar(n) que retorna true/false, como arrow function de uma linha.
4.2 — Escreva calcularIMC(peso, altura) que retorna o IMC com 1 casa decimal.
4.3 — Escreva aplicar(fn, lista) que recebe um callback e um array e retorna um novo array com o callback aplicado a cada item (sem usar map).
▸ Ver solução
// 4.1
const ehPar = n => n % 2 === 0;
// 4.2
const calcularIMC = (peso, altura) =>
Number((peso / (altura ** 2)).toFixed(1));
// 4.3
function aplicar(fn, lista) {
const resultado = [];
for (const item of lista) resultado.push(fn(item));
return resultado;
}
aplicar(n => n * 10, [1, 2, 3]); // [10, 20, 30]
05 Arrays e objetos
Objetivo: dominar as duas estruturas de dados que você usará em 100% dos projetos — e os métodos de array que separam iniciantes de profissionais.
Arrays: listas ordenadas
const tarefas = ["estudar", "treinar", "descansar"];
tarefas[0] // "estudar"
tarefas.length // 3
tarefas.push("ler") // adiciona no fim
tarefas.pop() // remove do fim
tarefas.unshift("acordar") // adiciona no início
tarefas.shift() // remove do início
tarefas.includes("ler") // true/false
tarefas.indexOf("treinar") // posição (ou -1)
tarefas.slice(0, 2) // cópia de um trecho (não altera o original)
tarefas.splice(1, 1) // remove/insere no meio (ALTERA o original)
O trio de ouro: map, filter e reduce
Estes três métodos aparecem em toda entrevista de frontend e backend. Eles transformam dados sem laços manuais e sem alterar o array original (imutabilidade — palavra-chave no React).
const precos = [10, 25, 40, 8];
// map: transforma cada item → novo array do MESMO tamanho
const comDesconto = precos.map(p => p * 0.9);
// [9, 22.5, 36, 7.2]
// filter: mantém só quem passa no teste → array MENOR ou igual
const caros = precos.filter(p => p > 20);
// [25, 40]
// reduce: resume tudo em UM valor
const total = precos.reduce((acc, p) => acc + p, 0);
// 83 (acc = acumulador; 0 = valor inicial)
// Encadeando (muito comum em código real):
const totalCarosComDesconto = precos
.filter(p => p > 20)
.map(p => p * 0.9)
.reduce((acc, p) => acc + p, 0); // 58.5
Outros métodos que caem em teste
[3, 7, 1].find(n => n > 5) // 7 (primeiro que passa)
[3, 7, 1].some(n => n > 5) // true (algum passa?)
[3, 7, 1].every(n => n > 0) // true (todos passam?)
[3, 7, 1].sort((a, b) => a - b) // [1, 3, 7] — sem o callback, ordena como TEXTO!
[1, [2, [3]]].flat(Infinity) // [1, 2, 3]
tarefas.forEach(t => console.log(t)) // percorre sem retornar nada
Objetos: dados com nome
const usuario = {
nome: "Lia",
idade: 28,
ativo: true,
endereco: { cidade: "BH", uf: "MG" }, // objetos aninhados
apresentar() { // método
return `Sou ${this.nome}`;
}
};
usuario.nome // "Lia" (notação de ponto)
usuario["idade"] // 28 (notação de colchete — chave dinâmica)
usuario.email = "a@b.com" // adiciona propriedade
delete usuario.ativo // remove
Object.keys(usuario) // ["nome", "idade", ...]
Object.values(usuario) // ["Lia", 28, ...]
Object.entries(usuario) // [["nome","Lia"], ["idade",28], ...]
Arrays de objetos — o formato dos dados reais
APIs, bancos de dados e telas de sistemas trabalham quase sempre com arrays de objetos. Treine muito este formato:
const produtos = [
{ id: 1, nome: "Teclado", preco: 120, estoque: 4 },
{ id: 2, nome: "Mouse", preco: 60, estoque: 0 },
{ id: 3, nome: "Monitor", preco: 900, estoque: 2 },
];
// nomes dos produtos disponíveis:
produtos
.filter(p => p.estoque > 0)
.map(p => p.nome); // ["Teclado", "Monitor"]
// valor total do estoque:
produtos.reduce((acc, p) => acc + p.preco * p.estoque, 0); // 2280
"Qual a diferença entre map e forEach?" — map retorna um novo array com o resultado do callback (transformação); forEach apenas executa o callback item a item e retorna undefined (efeitos colaterais). Se você precisa do resultado, é map; se só precisa "fazer algo" com cada item, é forEach.
5.1 — Com o array produtos acima: retorne os nomes dos produtos com preço abaixo de R$ 200, em letras maiúsculas.
5.2 — Calcule a média das notas [7, 8.5, 6, 9, 10] usando reduce.
5.3 — Dado um array de usuários {nome, idade}, retorne o mais velho usando reduce.
▸ Ver solução
// 5.1
produtos
.filter(p => p.preco < 200)
.map(p => p.nome.toUpperCase()); // ["TECLADO", "MOUSE"]
// 5.2
const notas = [7, 8.5, 6, 9, 10];
const media = notas.reduce((a, n) => a + n, 0) / notas.length; // 8.1
// 5.3
const maisVelho = usuarios.reduce(
(velho, u) => u.idade > velho.idade ? u : velho
);
JavaScript moderno e o navegador
Você já sabe a base da linguagem. Agora vem o JS que se escreve em empresas: sintaxe ES6+, manipulação de páginas reais e comunicação com servidores.
06 ES6+ essencial
Objetivo: dominar a sintaxe moderna que todo código de empresa usa — destructuring, spread, módulos e mais.
Destructuring (desestruturação)
// De objetos — usadíssimo em React e Node
const usuario = { nome: "Iuri", idade: 30, cidade: "POA" };
const { nome, idade } = usuario;
const { cidade: onde = "?" } = usuario; // renomeando + valor padrão
// De arrays
const [primeiro, segundo] = ["a", "b", "c"];
// Em parâmetros de função — padrão de mercado:
function criarPedido({ produto, quantidade = 1 }) {
return `${quantidade}x ${produto}`;
}
criarPedido({ produto: "café" }); // "1x café"
Spread e imutabilidade
const base = [1, 2, 3];
const copia = [...base, 4]; // [1,2,3,4] — novo array
const config = { tema: "claro", som: true };
const novaConfig = { ...config, tema: "escuro" };
// copia tudo e sobrescreve tema — padrão central no React!
Objetos e arrays são copiados por referência: const b = a não cria uma cópia — os dois apontam para o mesmo objeto. O spread {...a} cria uma cópia rasa (objetos aninhados continuam compartilhados). Para cópia profunda, use structuredClone(obj). Isso é pergunta frequente de entrevista.
Optional chaining e nullish coalescing
const pedido = { cliente: { nome: "Bia" } };
pedido.entrega?.cep // undefined (sem erro!) — ?. para acesso seguro
pedido.entrega.cep // ❌ TypeError
const qtd = pedido.qtd ?? 1; // ?? usa o padrão só se null/undefined
const qtd2 = pedido.qtd || 1; // || trata 0 e "" como "vazio" — cuidado
Módulos: import e export
Projetos reais são divididos em vários arquivos. Cada arquivo é um módulo que exporta o que quer tornar público.
utils.jsexport const TAXA = 0.1;
export function formatarMoeda(v) {
return v.toLocaleString("pt-BR", { style: "currency", currency: "BRL" });
}
export default function calcularTotal(itens) {
return itens.reduce((a, i) => a + i.preco, 0);
}
app.js
import calcularTotal, { TAXA, formatarMoeda } from "./utils.js";
// default entra sem chaves; nomeados entram com chaves
No HTML, use <script type="module" src="app.js"></script>. No Node.js, use a extensão .mjs ou defina "type": "module" no package.json.
"Qual a diferença entre || e ???" — || retorna o segundo valor se o primeiro for falsy (inclui 0, "", false); ?? só se for null ou undefined. Para valores numéricos onde 0 é válido (quantidade, desconto), ?? é o correto.
6.1 — Extraia titulo e autor (com padrão "desconhecido") de { titulo: "Dom Casmurro" } via destructuring.
6.2 — Sem alterar o array original [5, 3, 8], crie uma versão ordenada dele usando spread.
6.3 — Escreva pegarCidade(usuario) que retorna usuario.endereco.cidade ou "não informada", sem lançar erro para nenhum formato de entrada.
▸ Ver solução
// 6.1
const { titulo, autor = "desconhecido" } = { titulo: "Dom Casmurro" };
// 6.2
const original = [5, 3, 8];
const ordenado = [...original].sort((a, b) => a - b);
// 6.3
const pegarCidade = usuario =>
usuario?.endereco?.cidade ?? "não informada";
07 DOM e eventos
Objetivo: fazer o JavaScript controlar a página — selecionar elementos, reagir a cliques e criar interfaces dinâmicas. Este é o fundamento de todo o frontend (e do React).
O que é o DOM?
Quando o navegador carrega um HTML, ele monta na memória uma árvore de objetos chamada DOM (Document Object Model). Cada tag vira um objeto que o JS pode ler e modificar através da variável global document.
Selecionando elementos
// Os dois que você vai usar 95% do tempo (aceitam seletores CSS):
const titulo = document.querySelector("#titulo"); // primeiro que casa
const botoes = document.querySelectorAll(".btn"); // todos (NodeList)
// Lendo e alterando conteúdo:
titulo.textContent = "Novo título"; // só texto (seguro)
titulo.innerHTML = "<em>Novo</em>"; // interpreta HTML (cuidado: XSS!)
// Alterando estilo e classes:
titulo.style.color = "tomato";
titulo.classList.add("destaque");
titulo.classList.toggle("escondido"); // liga/desliga
// Atributos e inputs:
const campo = document.querySelector("#email");
campo.value // o que o usuário digitou
campo.setAttribute("disabled", "");
Criando e removendo elementos
const li = document.createElement("li");
li.textContent = "Nova tarefa";
document.querySelector("#lista").append(li); // insere no fim
li.remove(); // remove da página
Eventos: reagindo ao usuário
const btn = document.querySelector("#salvar");
btn.addEventListener("click", (evento) => {
console.log("clicou!", evento.target);
});
// Eventos mais usados:
// click, submit, input, change, keydown, mouseover, scroll, load
// Em formulários, previna o recarregamento da página:
form.addEventListener("submit", (e) => {
e.preventDefault();
// ... validar e enviar dados via JS
});
Exemplo completo: lista de tarefas mínima
index.html + app.js (junte tudo e abra no navegador)<input id="entrada" placeholder="Nova tarefa">
<button id="add">Adicionar</button>
<ul id="lista"></ul>
<script>
const entrada = document.querySelector("#entrada");
const lista = document.querySelector("#lista");
document.querySelector("#add").addEventListener("click", () => {
const texto = entrada.value.trim();
if (!texto) return; // validação: ignora vazio
const li = document.createElement("li");
li.textContent = texto;
li.addEventListener("click", () => li.remove()); // clicar apaga
lista.append(li);
entrada.value = "";
});
</script>
"O que é event delegation?" — Em vez de adicionar um listener em cada item de uma lista, adiciona-se um único listener no elemento pai e usa-se event.target para descobrir qual filho foi clicado. Funciona porque eventos "sobem" na árvore (bubbling). Vantagens: menos memória e funciona para itens criados depois.
lista.addEventListener("click", (e) => {
if (e.target.tagName === "LI") e.target.remove();
});
7.1 — Crie um contador com botões "+" e "−" que atualiza um número na tela (não deixe ficar negativo).
7.2 — Crie um campo de texto que mostra em tempo real quantos caracteres foram digitados (evento input).
7.3 — Adicione à lista de tarefas acima: tecla Enter também adiciona a tarefa.
▸ Ver solução
// 7.1
let contador = 0;
const span = document.querySelector("#valor");
document.querySelector("#mais").addEventListener("click", () => {
span.textContent = ++contador;
});
document.querySelector("#menos").addEventListener("click", () => {
if (contador > 0) span.textContent = --contador;
});
// 7.2
campo.addEventListener("input", () => {
contadorEl.textContent = `${campo.value.length} caracteres`;
});
// 7.3
entrada.addEventListener("keydown", (e) => {
if (e.key === "Enter") document.querySelector("#add").click();
});
08 Assincronia e consumo de APIs
Objetivo: entender promises e async/await e buscar dados de servidores reais com fetch — a habilidade mais exigida em vagas júnior.
Por que "assíncrono"?
Buscar dados de um servidor demora (milissegundos a segundos). Se o JS parasse para esperar, a página congelaria. Por isso, operações demoradas são assíncronas: o código pede a operação, continua rodando, e recebe o resultado depois.
Promises
Uma Promise representa um valor que ainda vai chegar. Ela tem 3 estados: pending (esperando), fulfilled (deu certo) e rejected (deu erro).
const promessa = new Promise((resolve, reject) => {
setTimeout(() => resolve("dados prontos!"), 1000);
});
promessa
.then(resultado => console.log(resultado)) // sucesso
.catch(erro => console.error(erro)) // falha
.finally(() => console.log("terminou")); // sempre roda
async / await — o jeito moderno
async/await é açúcar sintático sobre promises: o código assíncrono fica com cara de código normal, de cima para baixo.
async function buscarUsuario() {
try {
const resposta = await fetch("https://api.github.com/users/torvalds");
if (!resposta.ok) throw new Error(`HTTP ${resposta.status}`);
const dados = await resposta.json(); // converte o corpo em objeto
console.log(dados.name, dados.public_repos);
} catch (erro) {
console.error("Falha na busca:", erro.message);
}
}
buscarUsuario();
O fetch não rejeita a promise em erros HTTP (404, 500...) — só em falha de rede. Por isso a checagem if (!resposta.ok) throw ... é obrigatória em código profissional.
Enviando dados: POST com fetch
const resposta = await fetch("https://api.exemplo.com/pedidos", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ produto: "café", qtd: 2 }),
});
const criado = await resposta.json();
JSON (JavaScript Object Notation) é o formato de texto usado para trocar dados: JSON.stringify(obj) converte objeto → texto; JSON.parse(texto) faz o inverso.
Várias promises ao mesmo tempo
// Em paralelo (mais rápido que await um por um):
const [usuario, pedidos] = await Promise.all([
fetch("/api/usuario").then(r => r.json()),
fetch("/api/pedidos").then(r => r.json()),
]);
// Promise.all rejeita se QUALQUER uma falhar.
// Promise.allSettled espera todas e informa o status de cada uma.
"O que imprime e em que ordem?"
console.log("A");
setTimeout(() => console.log("B"), 0);
Promise.resolve().then(() => console.log("C"));
console.log("D");
Resposta: A, D, C, B. Código síncrono roda primeiro (A, D); depois as microtasks (promises → C); por último as macrotasks (setTimeout → B), mesmo com atraso 0. O porquê você verá no módulo 10 (event loop).
8.1 — Busque https://api.github.com/users/SEU_USUARIO e exiba nome e número de repositórios no console (com tratamento de erro).
8.2 — Escreva esperar(ms) que retorna uma promise resolvida após ms milissegundos, e use-a com await para imprimir "1"... "2"... "3" com 1s de intervalo.
8.3 — Busque 3 usuários do GitHub em paralelo com Promise.all e imprima seus nomes.
▸ Ver solução
// 8.2
const esperar = ms => new Promise(res => setTimeout(res, ms));
async function contar() {
for (let i = 1; i <= 3; i++) {
console.log(i);
await esperar(1000);
}
}
// 8.3
const logins = ["torvalds", "gaearon", "sindresorhus"];
const usuarios = await Promise.all(
logins.map(l =>
fetch(`https://api.github.com/users/${l}`).then(r => r.json())
)
);
usuarios.forEach(u => console.log(u.name));
JavaScript avançado
Os módulos a seguir são o que diferencia candidatos em entrevistas de nível pleno — e o que faz você entender por que o JS se comporta do jeito que se comporta.
09 POO, this e protótipos
Objetivo: modelar entidades com classes, entender de verdade o this e descobrir o que existe por baixo das classes: protótipos.
Classes
class ContaBancaria {
#saldo = 0; // # = campo privado (de verdade)
constructor(titular) {
this.titular = titular;
}
depositar(valor) {
if (valor <= 0) throw new Error("Valor inválido");
this.#saldo += valor;
return this; // permite encadear: conta.depositar(10).depositar(20)
}
get saldo() { // getter: acessado como propriedade
return this.#saldo;
}
static deTransferencia(origem, valor) { // método da classe, não da instância
return new ContaBancaria(origem).depositar(valor);
}
}
const conta = new ContaBancaria("Rui");
conta.depositar(100);
conta.saldo; // 100
conta.#saldo; // ❌ SyntaxError — privado mesmo
Herança
class ContaPoupanca extends ContaBancaria {
constructor(titular, taxa) {
super(titular); // chama o constructor do pai — obrigatório
this.taxa = taxa;
}
render() {
this.depositar(this.saldo * this.taxa);
}
}
Dica de arquitetura: o mercado hoje prefere composição (objetos que usam outros objetos) a longas cadeias de herança. Use herança para relações claras de "é um"; para o resto, componha.
O this — as 4 regras
this é o tema que mais derruba candidatos. Ele não depende de onde a função foi escrita, e sim de como ela foi chamada:
| Como foi chamada | this vale... |
|---|---|
obj.metodo() | O objeto antes do ponto (obj) |
funcao() solta | undefined em strict mode; window/globalThis fora dele |
new Funcao() | O novo objeto sendo criado |
fn.call(x) / fn.apply(x) / fn.bind(x) | Forçado a ser x |
Arrow functions não têm this próprio: herdam o this do escopo onde foram escritas. Por isso são perfeitas para callbacks dentro de métodos:
class Timer {
segundos = 0;
iniciar() {
setInterval(() => {
this.segundos++; // ✅ arrow herda o this da instância
}, 1000);
// com function () {...} aqui, this seria undefined/window ❌
}
}
Por baixo do capô: protótipos
Classes são "açúcar sintático". O mecanismo real do JS é a cadeia de protótipos: todo objeto tem um link interno para outro objeto (seu protótipo). Ao acessar obj.prop, o JS procura em obj; se não achar, sobe para o protótipo, e assim por diante até Object.prototype → null.
const animal = { respirar() { return "inspira... expira"; } };
const gato = Object.create(animal); // gato tem animal como protótipo
gato.respirar(); // "inspira... expira" — herdado!
Object.getPrototypeOf(gato) === animal; // true
// É assim que [1,2].map(...) funciona:
// o array não tem "map"; ele está em Array.prototype.
"O que é a cadeia de protótipos?" — É o mecanismo de herança do JS: a busca de propriedades sobe de objeto em objeto pelo link de protótipo até encontrar a propriedade ou chegar a null. Classes ES6 usam exatamente esse mecanismo por baixo. Saber explicar isso com um exemplo de Object.create impressiona.
9.1 — Crie a classe Produto (nome, preço) com o método aplicarDesconto(pct) que valida o percentual (0–100) e retorna this para encadeamento.
9.2 — Crie ProdutoDigital extends Produto com propriedade tamanhoMB e um getter descricao.
9.3 — Sem rodar, diga o que imprime: const f = conta.depositar; f(10); — e explique.
▸ Ver solução
// 9.1
class Produto {
constructor(nome, preco) {
this.nome = nome;
this.preco = preco;
}
aplicarDesconto(pct) {
if (pct < 0 || pct > 100) throw new Error("Percentual inválido");
this.preco *= (1 - pct / 100);
return this;
}
}
// 9.2
class ProdutoDigital extends Produto {
constructor(nome, preco, tamanhoMB) {
super(nome, preco);
this.tamanhoMB = tamanhoMB;
}
get descricao() {
return `${this.nome} (${this.tamanhoMB}MB) — R$ ${this.preco}`;
}
}
// 9.3 — Lança erro (TypeError). Ao guardar o método numa variável
// e chamar "solto", o this deixa de ser a instância (vira undefined
// em módulos/classes, que rodam em strict mode). Correção:
// const f = conta.depositar.bind(conta);
10 Closures, escopo e event loop
Objetivo: os três temas mais cobrados em entrevistas de médio/alto nível — hoisting, closures e o funcionamento interno da assincronia.
Hoisting e o problema do var
Antes de executar, o JS "registra" as declarações do escopo. Declarações function são içadas (hoisted) por inteiro; var é içado com valor undefined; let/const são içados mas ficam inacessíveis até a linha da declaração (a temporal dead zone).
console.log(a); // undefined (var içado sem valor)
var a = 1;
console.log(b); // ❌ ReferenceError (TDZ)
let b = 2;
oi(); // "oi!" — function declaration é içada inteira
function oi() { console.log("oi!"); }
Além disso, var ignora blocos (tem escopo de função) e pode ser redeclarado. É por isso que código moderno usa apenas let e const.
Closures
Uma closure é uma função que "lembra" das variáveis do escopo onde foi criada, mesmo depois que esse escopo terminou de executar. É o recurso que permite estado privado sem classes:
function criarContador() {
let total = 0; // variável "presa" na closure
return {
incrementar: () => ++total,
valor: () => total,
};
}
const c = criarContador();
c.incrementar();
c.incrementar();
c.valor(); // 2
c.total; // undefined — ninguém acessa por fora!
A pegadinha clássica do loop
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// imprime: 3, 3, 3 — todas as closures compartilham o MESMO i (var)
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// imprime: 0, 1, 2 — let cria um i NOVO a cada volta do loop
O event loop
O JavaScript executa uma coisa por vez (single-threaded), numa pilha de chamadas (call stack). Operações assíncronas são entregues ao ambiente (navegador/Node) e, quando terminam, seus callbacks entram em filas. O event loop é quem move callbacks das filas para a pilha — mas só quando a pilha está vazia.
- Roda todo o código síncrono (esvazia a pilha).
- Esvazia a fila de microtasks: callbacks de promises (
.then,await),queueMicrotask. - Pega uma macrotask:
setTimeout,setInterval, eventos de I/O. - Volta ao passo 2. Repete para sempre.
console.log("1"); // síncrono
setTimeout(() => console.log("2"), 0); // macrotask
Promise.resolve().then(() => console.log("3")); // microtask
queueMicrotask(() => console.log("4")); // microtask
console.log("5"); // síncrono
// Saída: 1, 5, 3, 4, 2
Consequência prática: um cálculo pesado síncrono congela a interface, porque nada mais roda enquanto a pilha não esvazia. Soluções: dividir o trabalho, usar Web Workers, ou mover para o backend.
Coerção de tipos (o lado estranho do JS)
"5" + 3 // "53" → + com string concatena
"5" - 3 // 2 → - só existe para números, converte
true + true // 2
[] + [] // "" → arrays viram string
NaN === NaN // false! Use Number.isNaN(x)
Você não vai escrever código assim — mas entrevistadores usam esses casos para testar se você entende as regras de conversão. Regra de bolso: + puxa para string se houver string; os demais operadores aritméticos puxam para número.
"O que é uma closure e onde você já usou uma?" — Definição: função que retém acesso ao escopo léxico em que foi criada. Exemplos reais para citar: manter estado privado (contador acima), debounce/throttle de eventos, callbacks que lembram parâmetros, e os hooks do React (que dependem de closures). Dar um exemplo do seu próprio código vale mais que a definição.
10.1 — Implemente criarCaixa() com métodos guardar(item) e listar(), onde o array interno seja inacessível por fora (closure).
10.2 — Implemente debounce(fn, ms): retorna uma versão de fn que só executa se ficar ms sem novas chamadas (usado em campos de busca!).
10.3 — Preveja a saída: console.log("a"); (async () => { console.log("b"); await null; console.log("c"); })(); console.log("d");
▸ Ver solução
// 10.1
function criarCaixa() {
const itens = [];
return {
guardar: item => itens.push(item),
listar: () => [...itens], // devolve cópia, protege o original
};
}
// 10.2
function debounce(fn, ms) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), ms);
};
}
// uso: input.addEventListener("input", debounce(buscar, 400));
// 10.3 → a, b, d, c
// O corpo do async roda síncrono até o primeiro await ("b");
// o await devolve o controle ("d" roda); "c" vira microtask.
11 Padrões e código profissional
Objetivo: tratamento de erros, organização, imutabilidade e os padrões que revisores de código esperam ver.
Tratamento de erros como gente grande
// Lançando erros específicos:
class ValidationError extends Error {
constructor(msg, campo) {
super(msg);
this.name = "ValidationError";
this.campo = campo;
}
}
function validarEmail(email) {
if (!email?.includes("@")) {
throw new ValidationError("E-mail inválido", "email");
}
}
try {
validarEmail("abc");
} catch (err) {
if (err instanceof ValidationError) {
console.warn(`Campo ${err.campo}: ${err.message}`);
} else {
throw err; // erro inesperado: não engula, repasse!
}
}
Regras de ouro: nunca deixe um catch vazio; trate o que sabe tratar e repasse o resto; mensagens de erro devem dizer o que falhou e como corrigir.
Imutabilidade na prática
Alterar dados "no lugar" (mutação) gera bugs difíceis, porque outras partes do código podem estar usando o mesmo objeto. O padrão profissional (obrigatório em React/Redux) é criar novas versões:
// ❌ mutação
usuario.idade = 31;
lista.push(item);
// ✅ imutável
const atualizado = { ...usuario, idade: 31 };
const novaLista = [...lista, item];
const semItem = lista.filter(i => i.id !== alvo);
const ordenada = [...lista].sort(cmp); // sort muta! copie antes
Padrões de projeto que você verá em código real
- Module pattern — cada arquivo expõe só o necessário via
export; o resto é privado. - Factory function — função que cria e retorna objetos configurados (alternativa leve a classes).
- Observer / pub-sub — objetos "assinam" eventos e são notificados; é o modelo do
addEventListenere do EventEmitter do Node. - Singleton — uma única instância compartilhada (ex.: conexão com banco). Em JS, um módulo já é naturalmente um singleton.
- Early return — valide e saia cedo, evitando
ifaninhados:
// ❌ pirâmide da perdição
function processar(pedido) {
if (pedido) {
if (pedido.itens.length > 0) {
if (pedido.pago) { /* ... */ }
}
}
}
// ✅ early return
function processar(pedido) {
if (!pedido) return;
if (pedido.itens.length === 0) return;
if (!pedido.pago) throw new Error("Pedido não pago");
/* caminho feliz sem indentação */
}
Estruturas além de array e objeto
// Map: chaves de qualquer tipo, ordem garantida, .size
const estoque = new Map([["teclado", 4], ["mouse", 9]]);
estoque.get("mouse"); // 9
// Set: coleção sem duplicatas — jeito idiomático de deduplicar:
const unicos = [...new Set([1, 2, 2, 3])]; // [1, 2, 3]
✔ nomes descritivos em um idioma só · ✔ const por padrão · ✔ === sempre · ✔ funções curtas de responsabilidade única · ✔ erros tratados, nunca engolidos · ✔ sem mutação desnecessária · ✔ sem código morto ou console.log esquecido · ✔ formatado com Prettier e lintado com ESLint.
11.1 — Refatore com early return: uma função podeDirigir(pessoa) que exige pessoa existente, idade ≥ 18 e habilitação válida.
11.2 — Dado um array de e-mails com duplicatas e espaços extras, retorne-os únicos, minúsculos e sem espaços (encadeie métodos + Set).
11.3 — Crie uma factory criarLogger(prefixo) que retorna { info, erro }, cada um imprimindo com o prefixo e a hora atual.
▸ Ver solução
// 11.1
function podeDirigir(pessoa) {
if (!pessoa) return false;
if (pessoa.idade < 18) return false;
return pessoa.habilitacaoValida === true;
}
// 11.2
const limpos = [...new Set(
emails.map(e => e.trim().toLowerCase())
)];
// 11.3
const criarLogger = prefixo => ({
info: msg => console.log(`[${prefixo}] ${new Date().toLocaleTimeString()} ${msg}`),
erro: msg => console.error(`[${prefixo}] ⚠ ${msg}`),
});
Backend com Node.js
Hora de virar fullstack: o mesmo JavaScript, agora criando servidores, APIs e conversando com bancos de dados.
12 Node.js e npm
Objetivo: entender o que é o Node, gerenciar dependências com npm e criar seus primeiros scripts de servidor.
O que é o Node.js?
Node.js é um ambiente de execução que roda JavaScript fora do navegador, usando o mesmo motor V8 do Chrome. Com ele, JS ganha acesso a arquivos, rede e sistema operacional — tudo que um servidor precisa. Diferenças em relação ao navegador: não existe document/window; em compensação existem módulos como fs (arquivos), http, path e os.
npm: o gerenciador de pacotes
O npm dá acesso ao maior ecossistema de bibliotecas do mundo. Todo projeto Node começa assim:
mkdir minha-api && cd minha-api
npm init -y # cria o package.json
npm install express # instala uma dependência
npm install -D nodemon # -D = só para desenvolvimento
package.json — a identidade do projeto
{
"name": "minha-api",
"type": "module",
"scripts": {
"dev": "nodemon server.js",
"start": "node server.js"
},
"dependencies": { "express": "^5.1.0" },
"devDependencies": { "nodemon": "^3.1.0" }
}
npm run devexecuta o script "dev" (nodemon reinicia o servidor a cada mudança no código).- A pasta
node_modulesguarda os pacotes — nunca vai para o Git (adicione ao.gitignore). - O
package-lock.jsontrava as versões exatas — este vai para o Git.
Lendo e escrevendo arquivos
import { readFile, writeFile } from "node:fs/promises";
const conteudo = await readFile("dados.json", "utf-8");
const dados = JSON.parse(conteudo);
dados.push({ id: 4, nome: "novo item" });
await writeFile("dados.json", JSON.stringify(dados, null, 2));
Um servidor HTTP sem bibliotecas
import { createServer } from "node:http";
const server = createServer((req, res) => {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ mensagem: "Servidor no ar!" }));
});
server.listen(3000, () => console.log("http://localhost:3000"));
Funciona, mas repare como seria trabalhoso lidar com rotas, métodos e erros manualmente. É por isso que existe o Express (próximo módulo).
Variáveis de ambiente
Senhas, chaves de API e configurações nunca ficam no código. Elas vivem em variáveis de ambiente, tipicamente num arquivo .env (que também vai para o .gitignore):
PORT=3000
DATABASE_URL=postgres://usuario:senha@localhost/minhabase
JWT_SECRET=troque-por-algo-aleatorio-e-grande
// Node 20+ lê .env nativamente: node --env-file=.env server.js
const porta = process.env.PORT ?? 3000;
"O Node é single-threaded. Como ele atende milhares de requisições?" — O JavaScript roda em uma thread, mas as operações de I/O (rede, disco, banco) são delegadas ao sistema e processadas de forma não bloqueante; o event loop entrega os callbacks quando os resultados chegam. Enquanto uma requisição espera o banco responder, o Node já atende outras. O gargalo do Node é CPU intensiva, não I/O.
12.1 — Crie um script notas.js que lê um arquivo notas.json (array de números), calcula a média e escreve o resultado em media.txt.
12.2 — Crie um projeto npm do zero com um script "ola" que roda um arquivo imprimindo seu nome.
12.3 — Modifique o servidor HTTP acima para responder {"hora": "..."} com a hora atual na rota /hora e 404 nas demais.
▸ Ver solução
// 12.3
const server = createServer((req, res) => {
if (req.url === "/hora" && req.method === "GET") {
res.writeHead(200, { "Content-Type": "application/json" });
return res.end(JSON.stringify({ hora: new Date().toISOString() }));
}
res.writeHead(404, { "Content-Type": "application/json" });
res.end(JSON.stringify({ erro: "Rota não encontrada" }));
});
13 APIs REST com Express
Objetivo: construir uma API REST de verdade — o tipo de projeto que aparece em 90% dos testes técnicos de backend júnior.
REST em 60 segundos
Uma API REST expõe recursos (usuários, produtos, tarefas...) através de URLs e usa os métodos HTTP para dizer a intenção:
| Método | Rota | Ação | Status típico |
|---|---|---|---|
| GET | /tarefas | Listar todas | 200 |
| GET | /tarefas/7 | Buscar uma | 200 / 404 |
| POST | /tarefas | Criar | 201 |
| PUT/PATCH | /tarefas/7 | Atualizar | 200 |
| DELETE | /tarefas/7 | Remover | 204 |
Códigos de status que você precisa saber de cor: 200 OK · 201 Criado · 204 Sem conteúdo · 400 Requisição inválida · 401 Não autenticado · 403 Sem permissão · 404 Não encontrado · 500 Erro interno.
API completa de tarefas
server.js (npm install express)import express from "express";
const app = express();
app.use(express.json()); // middleware: interpreta corpo JSON
let tarefas = [{ id: 1, titulo: "Estudar Express", feita: false }];
let proximoId = 2;
// LISTAR — com filtro opcional: /tarefas?feita=true
app.get("/tarefas", (req, res) => {
const { feita } = req.query;
if (feita !== undefined) {
return res.json(tarefas.filter(t => String(t.feita) === feita));
}
res.json(tarefas);
});
// BUSCAR UMA — parâmetro de rota
app.get("/tarefas/:id", (req, res) => {
const tarefa = tarefas.find(t => t.id === Number(req.params.id));
if (!tarefa) return res.status(404).json({ erro: "Tarefa não encontrada" });
res.json(tarefa);
});
// CRIAR — com validação
app.post("/tarefas", (req, res) => {
const { titulo } = req.body;
if (!titulo?.trim()) {
return res.status(400).json({ erro: "O campo 'titulo' é obrigatório" });
}
const nova = { id: proximoId++, titulo: titulo.trim(), feita: false };
tarefas.push(nova);
res.status(201).json(nova);
});
// ATUALIZAR
app.patch("/tarefas/:id", (req, res) => {
const tarefa = tarefas.find(t => t.id === Number(req.params.id));
if (!tarefa) return res.status(404).json({ erro: "Tarefa não encontrada" });
Object.assign(tarefa, req.body);
res.json(tarefa);
});
// REMOVER
app.delete("/tarefas/:id", (req, res) => {
tarefas = tarefas.filter(t => t.id !== Number(req.params.id));
res.status(204).end();
});
app.listen(3000, () => console.log("API em http://localhost:3000"));
Teste com o Postman, Insomnia ou direto no terminal:
curl http://localhost:3000/tarefas
curl -X POST http://localhost:3000/tarefas \
-H "Content-Type: application/json" \
-d '{"titulo": "Aprender curl"}'
Middlewares: o coração do Express
Um middleware é uma função que roda entre a chegada da requisição e a resposta: (req, res, next). Eles se encadeiam — cada um faz sua parte e chama next() para passar adiante. Autenticação, logs, CORS e tratamento de erros são todos middlewares.
// Log de todas as requisições:
app.use((req, res, next) => {
console.log(`${req.method} ${req.url}`);
next();
});
// Tratador central de erros (4 parâmetros, fica por ÚLTIMO):
app.use((err, req, res, next) => {
console.error(err);
res.status(500).json({ erro: "Erro interno do servidor" });
});
Organização profissional de pastas
minha-api/
├── src/
│ ├── routes/ # define as URLs (tarefas.routes.js)
│ ├── controllers/ # recebe req/res, chama serviços
│ ├── services/ # regras de negócio
│ ├── repositories/ # acesso ao banco de dados
│ └── middlewares/
├── server.js
├── .env
└── package.json
Essa separação em camadas (rota → controller → service → repository) é o que entrevistadores querem ouvir quando perguntam "como você organiza uma API?".
"O que é CORS e por que dá erro?" — Por segurança, navegadores bloqueiam requisições de um site para uma API em origem diferente (domínio/porta/protocolo), a menos que a API autorize via cabeçalhos Access-Control-Allow-*. No Express: npm install cors e app.use(cors()) (em produção, restrinja às origens do seu frontend). Importante: é uma proteção do navegador — o erro aparece no frontend, mas a correção é no backend.
13.1 — Adicione à API a rota GET /tarefas/stats retornando { total, feitas, pendentes }. (Atenção: declare-a ANTES de /tarefas/:id — descubra por quê.)
13.2 — Valide no PATCH que, se feita vier no corpo, seja booleano; senão, 400.
13.3 — Crie um middleware que rejeite com 401 qualquer requisição sem o cabeçalho x-api-key: 12345.
▸ Ver solução
// 13.1 — rotas específicas vêm antes das rotas com :parâmetro,
// senão "stats" seria interpretado como um :id.
app.get("/tarefas/stats", (req, res) => {
const feitas = tarefas.filter(t => t.feita).length;
res.json({ total: tarefas.length, feitas, pendentes: tarefas.length - feitas });
});
// 13.2
if ("feita" in req.body && typeof req.body.feita !== "boolean") {
return res.status(400).json({ erro: "'feita' deve ser booleano" });
}
// 13.3
const exigirApiKey = (req, res, next) => {
if (req.headers["x-api-key"] !== "12345") {
return res.status(401).json({ erro: "API key ausente ou inválida" });
}
next();
};
app.use(exigirApiKey);
14 Banco de dados e autenticação
Objetivo: persistir dados de verdade e proteger rotas com JWT — os dois pilares que faltam para sua API ser "de produção".
SQL ou NoSQL?
| SQL (PostgreSQL, MySQL) | NoSQL (MongoDB) | |
|---|---|---|
| Estrutura | Tabelas com colunas fixas | Documentos JSON flexíveis |
| Relações | JOINs poderosos, integridade forte | Dados aninhados no documento |
| Quando usar | Dados relacionais: e-commerce, financeiro (maioria dos casos) | Dados de formato variável, prototipagem rápida |
| Ferramenta JS típica | Prisma, Knex, pg | Mongoose |
Para o mercado: aprenda SQL básico de qualquer jeito (SELECT, INSERT, UPDATE, DELETE, JOIN, WHERE) — é cobrado até em vagas frontend. E conheça um ORM.
ORM na prática com Prisma
Um ORM (Object-Relational Mapper) deixa você manipular o banco com métodos JS em vez de SQL puro:
schema.prisma — você declara o modelo…model Usuario {
id Int @id @default(autoincrement())
email String @unique
senha String
tarefas Tarefa[]
}
model Tarefa {
id Int @id @default(autoincrement())
titulo String
feita Boolean @default(false)
usuario Usuario @relation(fields: [usuarioId], references: [id])
usuarioId Int
}
…e usa assim no código:
const tarefas = await prisma.tarefa.findMany({
where: { usuarioId: 1, feita: false },
orderBy: { id: "desc" },
});
const nova = await prisma.tarefa.create({
data: { titulo: "Aprender Prisma", usuarioId: 1 },
});
Nunca monte SQL concatenando strings com dados do usuário (`SELECT ... WHERE nome = '${nome}'`) — um atacante pode injetar comandos. Use sempre queries parametrizadas ou um ORM, que fazem isso por você. É pergunta certa em entrevistas de backend.
Autenticação com JWT
O fluxo padrão de login em APIs modernas:
- No cadastro, a senha nunca é salva pura: gera-se um hash com
bcrypt. - No login, compara-se a senha enviada com o hash. Se bater, o servidor assina um JWT (JSON Web Token) — um "crachá" com os dados do usuário e prazo de validade.
- O cliente envia o token em cada requisição:
Authorization: Bearer <token>. - Um middleware valida o token e libera (ou não) a rota.
import bcrypt from "bcrypt";
import jwt from "jsonwebtoken";
// cadastro
app.post("/registrar", async (req, res) => {
const { email, senha } = req.body;
const hash = await bcrypt.hash(senha, 10);
const usuario = await prisma.usuario.create({ data: { email, senha: hash } });
res.status(201).json({ id: usuario.id, email }); // nunca devolva a senha!
});
// login
app.post("/login", async (req, res) => {
const { email, senha } = req.body;
const usuario = await prisma.usuario.findUnique({ where: { email } });
const ok = usuario && await bcrypt.compare(senha, usuario.senha);
if (!ok) return res.status(401).json({ erro: "Credenciais inválidas" });
const token = jwt.sign({ sub: usuario.id }, process.env.JWT_SECRET, {
expiresIn: "1h",
});
res.json({ token });
});
// middleware de proteção
function exigirLogin(req, res, next) {
const token = req.headers.authorization?.split(" ")[1];
try {
req.usuarioId = jwt.verify(token, process.env.JWT_SECRET).sub;
next();
} catch {
res.status(401).json({ erro: "Token ausente ou inválido" });
}
}
app.get("/minhas-tarefas", exigirLogin, async (req, res) => {
res.json(await prisma.tarefa.findMany({ where: { usuarioId: req.usuarioId } }));
});
"Por que hashear senha em vez de criptografar?" — Criptografia é reversível (quem tem a chave descobre a senha); hash é uma via de mão única. Com bcrypt, nem o dono do banco consegue recuperar a senha original — no login, compara-se hash com hash. O bcrypt ainda é lento de propósito e usa salt (aleatoriedade por senha), o que inviabiliza ataques de dicionário em massa.
14.1 — Escreva o SQL para: criar a tabela tarefas(id, titulo, feita), inserir uma tarefa, listar as não feitas e marcar a de id 3 como feita.
14.2 — Na API do módulo 13, troque o array em memória por um arquivo tarefas.json (persistência simples com fs/promises).
14.3 — Adicione a regra: um usuário só pode ver/alterar as próprias tarefas (dica: use req.usuarioId em todos os where).
▸ Ver solução (14.1)
CREATE TABLE tarefas (
id SERIAL PRIMARY KEY,
titulo TEXT NOT NULL,
feita BOOLEAN DEFAULT FALSE
);
INSERT INTO tarefas (titulo) VALUES ('Estudar SQL');
SELECT * FROM tarefas WHERE feita = FALSE;
UPDATE tarefas SET feita = TRUE WHERE id = 3;
Rumo ao mercado
Você já sabe programar. Os módulos finais transformam esse conhecimento em empregabilidade: ferramentas do dia a dia, projetos de portfólio e preparação para o processo seletivo.
15 Ferramentas: Git, testes, TypeScript e React
Objetivo: conhecer as quatro ferramentas que mais aparecem em anúncios de vaga junto com "JavaScript" — o suficiente para começar e saber o que aprofundar.
Git e GitHub — inegociáveis
Todo time usa controle de versão. Os comandos que resolvem 90% do dia a dia:
git init # inicia um repositório
git status # o que mudou?
git add . # prepara as mudanças
git commit -m "feat: adiciona login" # salva um ponto na história
git log --oneline # histórico
git branch minha-feature # cria uma linha paralela de trabalho
git switch minha-feature # muda para ela
git merge minha-feature # traz as mudanças de volta
git clone URL # baixa um repositório
git push / git pull # envia / traz do GitHub
Fluxo profissional: cada tarefa vira uma branch; ao terminar, abre-se um Pull Request no GitHub para o time revisar antes de ir para a branch principal. Escreva mensagens de commit descritivas — recrutadores olham seu histórico. O padrão Conventional Commits (feat:, fix:, docs:, refactor:) é bem visto.
Testes automatizados
Testes provam que o código funciona e continuam provando a cada mudança. Vagas júnior raramente exigem maestria, mas saber o básico de Vitest ou Jest destaca você:
soma.test.js (npm i -D vitest → npx vitest)import { describe, it, expect } from "vitest";
import { somar } from "./soma.js";
describe("somar", () => {
it("soma dois números", () => {
expect(somar(2, 3)).toBe(5);
});
it("lida com negativos", () => {
expect(somar(-2, 2)).toBe(0);
});
});
Padrão de escrita: AAA — Arrange (prepara os dados), Act (executa), Assert (verifica). Teste primeiro o "caminho feliz", depois os casos de borda (vazio, nulo, negativo, limite).
TypeScript — JavaScript com tipos
TypeScript (TS) é um superconjunto do JS que adiciona tipos verificados antes de rodar. A maioria das vagas fullstack hoje pede TS — e a boa notícia: tudo desta apostila vale; você só adiciona anotações:
interface Produto {
id: number;
nome: string;
preco: number;
tags?: string[]; // ? = opcional
}
function aplicarDesconto(p: Produto, pct: number): Produto {
return { ...p, preco: p.preco * (1 - pct / 100) };
}
aplicarDesconto(produto, "10");
// ❌ erro ANTES de rodar: Argument of type 'string' is not
// assignable to parameter of type 'number'
Benefícios que você cita em entrevista: erros pegos em tempo de desenvolvimento, autocompletar preciso no editor, e tipos que documentam o código para o time. Comece convertendo um projeto pequeno seu para TS.
React — para onde o frontend converge
React é a biblioteca de interfaces mais pedida do mercado. A ideia central: a tela é uma função do estado — você declara como a interface deve ser para cada estado, e o React atualiza o DOM sozinho quando o estado muda.
import { useState } from "react";
function ListaDeTarefas() {
const [tarefas, setTarefas] = useState([]);
const [texto, setTexto] = useState("");
function adicionar() {
if (!texto.trim()) return;
setTarefas([...tarefas, { id: Date.now(), texto }]); // imutabilidade!
setTexto("");
}
return (
<div>
<input value={texto} onChange={e => setTexto(e.target.value)} />
<button onClick={adicionar}>Adicionar</button>
<ul>
{tarefas.map(t => <li key={t.id}>{t.texto}</li>)}
</ul>
</div>
);
}
Repare: map, spread, destructuring, arrow functions, closures — React é 80% JavaScript bem sabido. É exatamente por isso que esta apostila insiste tanto nos fundamentos. Quando for estudar React a fundo: componentes e props → estado (useState) → efeitos (useEffect) → consumo de API → rotas (React Router).
JavaScript/TypeScript + React no frontend + Node/Express no backend + PostgreSQL + Git + noções de testes e Docker. Você não precisa dominar tudo antes de se candidatar: vagas júnior esperam solidez nos fundamentos e capacidade de aprender o resto.
15.1 — Crie um repositório no GitHub, suba um dos seus exercícios com pelo menos 3 commits de mensagens descritivas e um README.
15.2 — Escreva testes para a função ehPar do módulo 4 cobrindo: par, ímpar, zero e negativo.
15.3 — Converta a função calcularIMC para TypeScript com tipos nos parâmetros e no retorno.
▸ Ver solução (15.2 e 15.3)
// 15.2
describe("ehPar", () => {
it("retorna true para pares", () => expect(ehPar(4)).toBe(true));
it("retorna false para ímpares", () => expect(ehPar(7)).toBe(false));
it("zero é par", () => expect(ehPar(0)).toBe(true));
it("funciona com negativos", () => expect(ehPar(-2)).toBe(true));
});
// 15.3
function calcularIMC(peso: number, altura: number): number {
return Number((peso / altura ** 2).toFixed(1));
}
16 Projetos completos para o portfólio
Objetivo: três projetos em ordem de dificuldade, com escopo definido e critérios de "pronto". Projetos valem mais que certificados — são a prova concreta do que você sabe.
Projeto 1 — Painel de Hábitos (frontend puro)
O que é: uma página onde o usuário cadastra hábitos diários (beber água, ler, treinar) e marca o cumprimento dia a dia, com estatísticas de sequência.
Pratica: DOM, eventos, arrays de objetos, map/filter/reduce, localStorage, datas.
Requisitos (critérios de pronto):
- Adicionar, renomear e excluir hábitos (com confirmação).
- Grade dos últimos 7 dias por hábito, com marcação clicável.
- Estatísticas calculadas com reduce: % de conclusão da semana e maior sequência.
- Dados persistidos em
localStorage(JSON.stringify/parse) — sobrevivem ao F5. - Validação: sem hábitos vazios ou duplicados; interface responsiva.
// Modelo de dados sugerido:
{
id: "a1b2",
nome: "Beber 2L de água",
registros: ["2026-07-05", "2026-07-06"] // dias cumpridos (ISO)
}
Projeto 2 — API de Finanças Pessoais (backend)
O que é: uma API REST para registrar receitas e despesas, com categorias, resumo mensal e autenticação.
Pratica: Express, camadas (routes/controllers/services), validação, JWT, banco de dados com Prisma, variáveis de ambiente.
Requisitos:
- CRUD de transações:
{ descricao, valor, tipo: "receita"|"despesa", categoria, data }. - Cadastro e login com bcrypt + JWT; cada usuário só acessa suas transações.
GET /resumo?mes=2026-07→ totais por tipo e por categoria (reduce no service).- Validações com respostas 400 claras; tratador central de erros; status codes corretos.
- README com instruções de instalação e tabela de rotas; coleção do Postman/Insomnia exportada no repositório.
Projeto 3 — Mural de Avisos Fullstack
O que é: um mini-fórum: usuários se cadastram, publicam avisos/recados e comentam nos dos outros. Frontend consome sua própria API.
Pratica: integração completa frontend ↔ backend, fetch com token, CORS, deploy.
Requisitos:
- Backend: API REST com usuários, posts e comentários (relações no banco).
- Frontend: telas de login/cadastro, feed com posts e comentários, criação e exclusão dos próprios posts. Pode ser JS puro ou React — fazer em React vale mais para o portfólio.
- O token JWT é guardado no cliente e enviado em cada fetch; expirou → volta ao login.
- Estados de interface: carregando, erro e lista vazia tratados visualmente.
- Deploy público: frontend no Vercel/Netlify, backend no Render/Railway, banco no Neon/Supabase (todos têm plano gratuito). Link no topo do README.
1) README caprichado: o que é, screenshot/GIF, tecnologias, como rodar, link do deploy. 2) Commits pequenos e descritivos — mostram seu processo. 3) Deploy funcionando — recrutador não clona repositório. 4) Tratamento de erros e validações — é onde júnior demonstra maturidade. 5) Um projeto seu (tema pessoal) vale mais que o 500º clone de Netflix.
• Adicione testes ao service de resumo do Projeto 2. • Adicione paginação (?pagina=2&limite=10) ao feed do Projeto 3. • Refatore um dos projetos para TypeScript. • Adicione um Dockerfile ao backend. Cada um desses itens rende assunto bom em entrevista.
17 Entrevista técnica
Objetivo: saber o que esperar de cada etapa do processo seletivo e revisar as perguntas que mais se repetem — com a estratégia de resposta.
As etapas típicas de um processo
- Triagem com RH — sua história, motivação, pretensão salarial. Tenha um "pitch" de 1 minuto pronto.
- Entrevista técnica de conversa — perguntas conceituais (as desta apostila).
- Desafio técnico — um projeto para casa (geralmente uma API ou uma tela consumindo API) ou live coding.
- Entrevista final / cultural — com o time ou liderança.
Top perguntas de JavaScript (revisão relâmpago)
| Pergunta | Núcleo da resposta | Módulo |
|---|---|---|
== vs === | === não faz coerção de tipos | 03 |
null vs undefined | intencional vs. "nunca atribuído" | 02 |
let / const / var | escopo de bloco vs. função; hoisting; TDZ | 10 |
| O que é closure? | função que lembra o escopo onde nasceu + exemplo seu | 10 |
| Como funciona o event loop? | pilha → microtasks → 1 macrotask → repete | 10 |
map vs forEach | retorna novo array vs. undefined | 05 |
O que é this? | definido pela forma de chamada; arrow herda | 09 |
| Promise vs async/await | mesma coisa; await é açúcar sintático + try/catch | 08 |
| Cadeia de protótipos | busca de propriedades sobe pelos protótipos | 09 |
| Cópia rasa vs. profunda | referência; spread é raso; structuredClone | 06 |
Perguntas de frontend
- "O que acontece quando você digita uma URL e aperta Enter?" — DNS resolve o domínio → conexão TCP/TLS → requisição HTTP → servidor responde HTML → navegador monta o DOM, baixa CSS/JS → renderiza. Responder em camadas mostra visão do todo.
- "O que é event delegation?" — um listener no pai +
event.target(módulo 7). - "Como você otimizaria uma página lenta?" — medir primeiro (Lighthouse/DevTools); depois: comprimir imagens, code splitting, cache, evitar bloqueio do main thread, debounce em eventos frequentes.
Perguntas de backend
- "Quais status codes você usaria em cada operação de um CRUD?" — tabela do módulo 13.
- "Como você protege uma API?" — autenticação (JWT), validação de entrada, hash de senhas, HTTPS, rate limiting, queries parametrizadas contra SQL injection, CORS restrito, segredos em variáveis de ambiente.
- "REST: o que significa ser stateless?" — o servidor não guarda estado da sessão entre requisições; cada requisição carrega tudo que precisa (por isso o token vai em todas).
- "Node é single-threaded — como escala?" — I/O não bloqueante + event loop (módulo 12); para CPU pesada: worker threads ou mais instâncias.
Live coding: como se comportar
1. Entenda antes de codar: repita o problema com suas palavras e pergunte sobre casos de borda ("e se o array vier vazio?"). Perguntar é ponto positivo, não fraqueza. 2. Pense alto: o avaliador julga seu raciocínio, não só o resultado. 3. Comece pela solução simples que funciona; otimize depois se der tempo. 4. Teste com um exemplo na mão, linha a linha. 5. Travou? Diga o que você sabe, o que tentaria e por quê — silêncio é o único erro fatal.
Exercícios de aquecimento clássicos que valem treinar antes: FizzBuzz (mód. 3), inverter string, verificar palíndromo, contar frequência de palavras (reduce + objeto), achatar array, remover duplicatas, e o famoso "some dois números do array que dão o alvo" (two sum).
// Palíndromo — resposta de 1 linha que impressiona:
const ehPalindromo = s => {
const limpo = s.toLowerCase().replace(/[^a-z0-9]/g, "");
return limpo === [...limpo].reverse().join("");
};
// Frequência de palavras — reduce + objeto:
const frequencia = frase =>
frase.toLowerCase().split(" ").reduce((acc, p) => {
acc[p] = (acc[p] ?? 0) + 1;
return acc;
}, {});
Perguntas comportamentais
Prepare 3–4 histórias reais no formato STAR (Situação, Tarefa, Ação, Resultado): um problema difícil que resolveu, um erro que cometeu e o que aprendeu, um feedback que recebeu, um projeto do qual se orgulha. Para quem está começando, histórias dos projetos da apostila contam — "no meu projeto de API de finanças, enfrentei X e resolvi com Y".
E tenha suas perguntas para o final ("Como é o dia a dia do time?", "Como funciona o processo de code review?", "O que diferencia quem cresce rápido aqui?") — não perguntar nada passa desinteresse.
"Por que devemos te contratar sem experiência profissional?" — Estratégia: aponte para evidências concretas (projetos com deploy, código no GitHub, consistência de estudo), demonstre que sabe o básico bem-feito (fundamentos > frameworks decorados) e mostre fome de aprender com exemplos reais. Confiança sem arrogância.
17.1 — Responda em voz alta, sem consultar: o que é closure, como funciona o event loop e a diferença entre == e ===. Grave-se e ouça.
17.2 — Implemente inverterString(s) e removerDuplicatas(arr) de duas formas cada (com e sem métodos prontos).
17.3 — Resolva o two sum: dado [2, 7, 11, 15] e alvo 9, retorne os índices dos dois números que somam o alvo.
▸ Ver solução
// 17.2
const inverterString = s => [...s].reverse().join("");
const inverterString2 = s => {
let r = "";
for (let i = s.length - 1; i >= 0; i--) r += s[i];
return r;
};
const removerDuplicatas = arr => [...new Set(arr)];
const removerDuplicatas2 = arr =>
arr.filter((item, i) => arr.indexOf(item) === i);
// 17.3 — O(n) com Map: guarda "quanto falta" → índice
function twoSum(nums, alvo) {
const vistos = new Map();
for (let i = 0; i < nums.length; i++) {
const falta = alvo - nums[i];
if (vistos.has(falta)) return [vistos.get(falta), i];
vistos.set(nums[i], i);
}
return [];
}
twoSum([2, 7, 11, 15], 9); // [0, 1]
18 Portfólio e carreira
Objetivo: montar a vitrine que fará recrutadores te chamarem — GitHub, LinkedIn, currículo — e traçar o plano dos próximos meses.
GitHub: seu portfólio de fato
- Perfil: foto, bio curta ("Dev fullstack JS em formação — React · Node · PostgreSQL"), e um README de perfil (repositório com seu nome de usuário) apresentando você e seus 3 melhores projetos com links.
- Fixe (pin) os 3 projetos do módulo 16 — qualidade acima de quantidade. Vinte repositórios de exercício soltos valem menos que três projetos completos com deploy.
- README de cada projeto: título e descrição de uma linha, screenshot ou GIF, link do deploy no topo, tecnologias, como rodar localmente, e uma seção "decisões técnicas" (por que Prisma? por que JWT?) — isso rende conversa em entrevista.
- Contribuições consistentes: commits regulares mostram constância. Não precisa ser todo dia — precisa ser real.
LinkedIn e currículo
- Título: "Desenvolvedor(a) Fullstack Júnior | JavaScript · React · Node.js" — recrutadores buscam por palavras-chave.
- Sobre: 3–5 linhas: quem você é, o que constrói, tecnologias, link do GitHub/portfólio. Sem clichês ("apaixonado por tecnologia") — mostre com fatos.
- Experiência sem emprego na área? Liste os projetos como experiência: "Projeto pessoal — API de finanças (Node, Express, PostgreSQL, JWT)" com 2 bullets de resultados concretos.
- Currículo de 1 página: contato + links, resumo, habilidades técnicas agrupadas, projetos (com links!), formação. PDF, sem foto de festa, sem "nível avançado em Word".
- Publique: um post curto por semana sobre o que aprendeu atrai recrutadores organicamente. Ensinar é a melhor forma de fixar.
Como e onde se candidatar
- LinkedIn Jobs, Gupy, programathor, remotar.com.br e as páginas "trabalhe conosco" de empresas que você admira.
- Candidate-se mesmo sem cumprir 100% dos requisitos — vagas júnior pedindo "2 anos de experiência" recebem sim candidatos sem isso. A lista de requisitos é um desejo, não um filtro rígido.
- Personalize as primeiras linhas da candidatura citando a empresa; mande mensagem educada para recrutadores da vaga com seu pitch + link do portfólio.
- Trate rejeição como estatística: dezenas de "não" antes do primeiro "sim" é o caminho normal. Peça feedback sempre.
Roadmap sugerido (recapitulando a jornada)
| Fase | Foco | Duração típica |
|---|---|---|
| 1. Fundamentos | Módulos 1–5 + muitos exercícios de lógica | 3–4 semanas |
| 2. Frontend | Módulos 6–8 + Projeto 1 no ar | 3–4 semanas |
| 3. Avançado | Módulos 9–11 (revisitar depois também) | 2 semanas |
| 4. Backend | Módulos 12–14 + Projeto 2 no ar | 3–4 semanas |
| 5. Mercado | Módulos 15–18 + Projeto 3 + candidaturas | contínuo |
| 6. Aprofundar | React a fundo, TypeScript, testes, SQL, Docker | contínuo |
Onde continuar estudando
- MDN Web Docs (developer.mozilla.org) — a documentação oficial da web, em português. Sua referência para sempre.
- javascript.info — o melhor aprofundamento gratuito da linguagem.
- roadmap.sh — mapas visuais de carreira (frontend, backend, fullstack).
- freeCodeCamp e The Odin Project — trilhas práticas gratuitas.
- Exercism e Codewars — exercícios diários para manter o músculo.
Consistência vence intensidade: 1h30 por dia durante meses constrói uma carreira; 10 horas num sábado, não. Programar se aprende programando — errando, lendo mensagens de erro, pesquisando e tentando de novo. Todo dev sênior já foi alguém que não sabia o que era uma variável. Boa jornada! 🚀