Apostila completa · Português (BR) · Edição 2026

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.

Frontend Backend / Node.js Exercícios + soluções 3 projetos Entrevistas
Como usar esta apostila

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:

Preparando o ambiente

  1. Instale o VS Code (editor de código gratuito): code.visualstudio.com.
  2. Instale o Node.js (versão LTS): nodejs.org. Ele permite rodar JS fora do navegador — essencial para o backend.
  3. 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
Visão de mercado

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.

Exercícios

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

TipoExemploPara quê
string"olá", 'oi', `texto`Textos
number42, 3.14, -7Números (inteiros e decimais são o mesmo tipo)
booleantrue, falseVerdadeiro/falso — base de toda decisão
undefinedlet x;Variável declarada sem valor
nulllet y = null;Ausência intencional de valor
bigint9007199254740993nInteiros gigantes
symbolSymbol("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"
Pergunta de entrevista

"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.

Exercícios

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
Valores falsy — decore esta lista

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
Pergunta de entrevista

"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.

Exercícios

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
Boas práticas que recrutadores notam

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.

Exercícios

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
Pergunta de entrevista

"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.

Exercícios

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
);
Parte II

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!
Cópia rasa vs. profunda (pega muita gente)

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.js
export 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.

Pergunta de entrevista

"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.

Exercícios

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>
Pergunta de entrevista

"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();
});
Exercícios

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();
Detalhe que reprova em teste técnico

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.
Pergunta de entrevista

"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).

Exercícios

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));
Parte III

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 chamadathis vale...
obj.metodo()O objeto antes do ponto (obj)
funcao() soltaundefined 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.prototypenull.

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.
Pergunta de entrevista

"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.

Exercícios

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.

  1. Roda todo o código síncrono (esvazia a pilha).
  2. Esvazia a fila de microtasks: callbacks de promises (.then, await), queueMicrotask.
  3. Pega uma macrotask: setTimeout, setInterval, eventos de I/O.
  4. 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.

Pergunta de entrevista

"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.

Exercícios

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

// ❌ 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]
Checklist de código que passa em code review

✔ 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.

Exercícios

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}`),
});
Parte IV

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" }
}

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):

.env
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;
Pergunta de entrevista

"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.

Exercícios

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étodoRotaAçãoStatus típico
GET/tarefasListar todas200
GET/tarefas/7Buscar uma200 / 404
POST/tarefasCriar201
PUT/PATCH/tarefas/7Atualizar200
DELETE/tarefas/7Remover204

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?".

Pergunta de entrevista

"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.

Exercícios

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)
EstruturaTabelas com colunas fixasDocumentos JSON flexíveis
RelaçõesJOINs poderosos, integridade forteDados aninhados no documento
Quando usarDados relacionais: e-commerce, financeiro (maioria dos casos)Dados de formato variável, prototipagem rápida
Ferramenta JS típicaPrisma, Knex, pgMongoose

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 },
});
Segurança: SQL Injection

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:

  1. No cadastro, a senha nunca é salva pura: gera-se um hash com bcrypt.
  2. 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.
  3. O cliente envia o token em cada requisição: Authorization: Bearer <token>.
  4. 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 } }));
});
Pergunta de entrevista

"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.

Exercícios

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;
Parte V

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).

A stack júnior fullstack mais contratável (Brasil, 2026)

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.

Exercícios

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):

// 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:

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:

O que separa um projeto de portfólio amador de um profissional

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.

Desafios extras (após concluir os 3)

• 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

  1. Triagem com RH — sua história, motivação, pretensão salarial. Tenha um "pitch" de 1 minuto pronto.
  2. Entrevista técnica de conversa — perguntas conceituais (as desta apostila).
  3. Desafio técnico — um projeto para casa (geralmente uma API ou uma tela consumindo API) ou live coding.
  4. Entrevista final / cultural — com o time ou liderança.

Top perguntas de JavaScript (revisão relâmpago)

PerguntaNúcleo da respostaMódulo
== vs ====== não faz coerção de tipos03
null vs undefinedintencional vs. "nunca atribuído"02
let / const / varescopo de bloco vs. função; hoisting; TDZ10
O que é closure?função que lembra o escopo onde nasceu + exemplo seu10
Como funciona o event loop?pilha → microtasks → 1 macrotask → repete10
map vs forEachretorna novo array vs. undefined05
O que é this?definido pela forma de chamada; arrow herda09
Promise vs async/awaitmesma coisa; await é açúcar sintático + try/catch08
Cadeia de protótiposbusca de propriedades sobe pelos protótipos09
Cópia rasa vs. profundareferência; spread é raso; structuredClone06

Perguntas de frontend

Perguntas de backend

Live coding: como se comportar

O método que os avaliadores querem ver

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.

A pergunta que sempre cai

"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.

Exercícios

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

LinkedIn e currículo

Como e onde se candidatar

Roadmap sugerido (recapitulando a jornada)

FaseFocoDuração típica
1. FundamentosMódulos 1–5 + muitos exercícios de lógica3–4 semanas
2. FrontendMódulos 6–8 + Projeto 1 no ar3–4 semanas
3. AvançadoMódulos 9–11 (revisitar depois também)2 semanas
4. BackendMódulos 12–14 + Projeto 2 no ar3–4 semanas
5. MercadoMódulos 15–18 + Projeto 3 + candidaturascontínuo
6. AprofundarReact a fundo, TypeScript, testes, SQL, Dockercontínuo

Onde continuar estudando

Última dica da apostila

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! 🚀