Trilha completa · 16 módulos

Desenvolvimento Full Stack com TypeScript e Next.js

Uma trilha estruturada que parte do zero absoluto e chega a tópicos de nível sênior: tipos avançados, React Server Components, Server Actions, Prisma, autenticação, testes, arquitetura, performance e deploy — sempre conectando cada assunto ao que as empresas realmente pedem em vagas.

TypeScript 5+ Next.js 15 · App Router React 19 Prisma · PostgreSQL Foco em empregabilidade
M00

Como usar esta apostila

Nível: todos · Tempo sugerido: 1h

Esta apostila foi desenhada como uma trilha de estudo orientada a empregabilidade. Cada módulo tem teoria, código comentado, exercícios e um projeto incremental que vira peça de portfólio.

A trilha em 4 fases

FaseMódulosO que você conquistaVagas compatíveis
FundaçãoM00–M02Lógica, ambiente, TypeScript sólidoEstágio
Front-endM03–M06React + Next.js modernos (App Router)Júnior front-end
Back-endM07–M09APIs, banco de dados, autenticaçãoJúnior/Pleno full stack
Avançado + CarreiraM10–M15Tipos avançados, testes, arquitetura, deployPleno/Sênior

Método de estudo recomendado

  1. Leia digitando: nunca copie e cole os exemplos. Digitar cria memória muscular e força você a ler os erros do compilador.
  2. Quebre o código de propósito: mude um tipo, remova um await, veja o que o TypeScript e o Next.js reclamam. Entender erros é 50% do trabalho real.
  3. Faça os exercícios antes de abrir a resposta (eles ficam em blocos recolhíveis).
  4. Commite tudo no GitHub: recrutadores olham consistência de commits mais do que você imagina.
💼 Visão de mercado

TypeScript + Next.js é hoje uma das combinações mais pedidas em vagas full stack no Brasil e no exterior (especialmente em startups, fintechs e agências). A stack aparece com nomes como "React/Next", "Node + TS" ou "T3 stack". Dominar o App Router e Server Components diferencia você da maioria dos candidatos, que só conhece o modelo antigo (Pages Router).

🚀 Projeto-guia da apostila: "DevBoard"

Ao longo dos módulos você construirá o DevBoard — um SaaS de gerenciamento de tarefas/projetos com autenticação, banco de dados, dashboard e API pública. É um projeto com cara de produto real, ideal para portfólio:

  • M04–M06: interface, rotas, listagem com dados reais
  • M07–M08: API tipada + Prisma + PostgreSQL
  • M09: login com credenciais e OAuth (GitHub)
  • M11–M14: testes, refino de arquitetura e deploy com CI/CD
M01

Fundamentos da Web e Node.js

Nível: básico · Tempo sugerido: 1 semana

Antes de qualquer framework, você precisa entender o que acontece quando alguém digita uma URL: cliente, servidor, HTTP e JavaScript rodando dos dois lados.

1.1 Como a web funciona

Toda aplicação web é uma conversa entre um cliente (navegador) e um servidor. A conversa usa o protocolo HTTP: o cliente envia uma requisição (request) e o servidor devolve uma resposta (response).

Método HTTPUso típicoExemplo
GETBuscar dadosListar produtos
POSTCriar recursoCadastrar usuário
PUT / PATCHAtualizar recursoEditar perfil
DELETERemover recursoExcluir tarefa

Os status codes mais cobrados em entrevistas: 200 (OK), 201 (criado), 301/308 (redirecionamento), 400 (requisição inválida), 401 (não autenticado), 403 (sem permissão), 404 (não encontrado), 422 (dados inválidos), 500 (erro do servidor).

1.2 O papel de cada tecnologia

  • HTML — estrutura e semântica do conteúdo.
  • CSS — aparência e layout (no mercado atual, muito via Tailwind CSS).
  • JavaScript — comportamento; a única linguagem nativa do navegador.
  • Node.js — ambiente que executa JavaScript fora do navegador (no servidor).
  • TypeScript — JavaScript com tipos, compilado para JavaScript.

1.3 Preparando o ambiente

Você vai precisar de: Node.js LTS (versão 20+), VS Code (ou Cursor), Git e uma conta no GitHub. Instale o Node preferencialmente via um gerenciador de versões:

terminalbash
# Instalar o nvm (Linux/macOS) e o Node LTS
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash
nvm install --lts
node -v   # deve mostrar v20.x ou superior
npm -v

# No Windows: use o nvm-windows ou o instalador oficial do nodejs.org

Extensões essenciais do VS Code

  • ESLint e Prettier — padrão de código (toda empresa usa).
  • Tailwind CSS IntelliSense — autocompletar de classes.
  • Pretty TypeScript Errors — erros de tipo legíveis.

1.4 JavaScript moderno que você precisa dominar

O TypeScript é construído sobre o JavaScript moderno (ES2015+). Estes recursos aparecem o tempo todo em código de produção:

js-moderno.jsJavaScript
// 1. const/let (nunca use var)
const nome = "Ana";      // não pode ser reatribuída
let contador = 0;       // pode ser reatribuída

// 2. Arrow functions
const dobrar = (n) => n * 2;

// 3. Template literals
const saudacao = `Olá, ${nome}! Você tem ${contador} pontos.`;

// 4. Destructuring
const usuario = { id: 1, email: "ana@ex.com", plano: "pro" };
const { email, plano } = usuario;

// 5. Spread e rest
const atualizado = { ...usuario, plano: "enterprise" };
const somar = (...nums) => nums.reduce((acc, n) => acc + n, 0);

// 6. Métodos de array (o coração do React!)
const precos = [10, 25, 40];
const comDesconto = precos.map((p) => p * 0.9);
const caros = precos.filter((p) => p > 20);
const total = precos.reduce((acc, p) => acc + p, 0);

// 7. Promises e async/await
async function buscarUsuario(id) {
  const res = await fetch(`https://api.exemplo.com/users/${id}`);
  if (!res.ok) throw new Error("Falha na requisição");
  return res.json();
}

// 8. Optional chaining e nullish coalescing
const cidade = usuario.endereco?.cidade ?? "Não informada";

// 9. Módulos ES
export function formatarMoeda(v) { return `R$ ${v.toFixed(2)}`; }
// import { formatarMoeda } from "./utils";
⚠️ Erro comum de iniciante

Pular direto para React/Next.js sem dominar map, filter, destructuring e async/await. Em entrevistas de júnior, a maioria das reprovações acontece em JavaScript básico, não no framework.

1.5 Git essencial para o dia a dia

terminalbash
git init                          # inicia o repositório
git add .                         # prepara as mudanças
git commit -m "feat: cria header" # salva um ponto na história
git branch -M main
git remote add origin git@github.com:voce/projeto.git
git push -u origin main

# fluxo de trabalho em equipe (o que cai em entrevista)
git checkout -b feat/login        # cria branch de feature
git push origin feat/login        # sobe e abre Pull Request
git pull --rebase origin main     # atualiza sua branch
💼 Visão de mercado

Use Conventional Commits (feat:, fix:, chore:, refactor:) desde o primeiro projeto. Além de organizar o histórico, sinaliza maturidade profissional no seu GitHub — e muitas empresas exigem esse padrão em CI.

Exercícios do módulo 01
  1. Escreva uma função agruparPorPlano(usuarios) que recebe um array de usuários e retorna um objeto { pro: [...], free: [...] } usando reduce.
  2. Usando fetch e async/await, consuma https://api.github.com/users/SEU_USUARIO e imprima nome e número de repositórios públicos. Trate erros com try/catch.
  3. Crie um repositório no GitHub com README explicando o que você está estudando e faça 3 commits com Conventional Commits.
M02

TypeScript essencial

Nível: básico → intermediário · Tempo sugerido: 2 semanas

TypeScript é JavaScript com um sistema de tipos estático. Ele não muda como seu código roda — muda quantos bugs chegam à produção e quão rápido você navega em bases de código grandes.

2.1 Por que empresas exigem TypeScript

  • Menos bugs: erros como "undefined is not a function" são pegos antes de rodar.
  • Autocomplete e refatoração seguros: renomear uma função atualiza todos os usos.
  • Documentação viva: os tipos explicam a intenção do código.
  • Escala de time: em times grandes, tipos são o contrato entre pessoas.

2.2 Primeiro contato

terminalbash
mkdir ts-lab && cd ts-lab
npm init -y
npm install -D typescript tsx
npx tsc --init          # cria o tsconfig.json
npx tsx arquivo.ts      # roda TypeScript direto, sem compilar manualmente

2.3 Tipos primitivos e inferência

basico.tsTypeScript
// Anotação explícita
let idade: number = 25;
let nome: string = "Ana";
let ativo: boolean = true;

// Inferência: o TS deduz o tipo sozinho — prefira isso!
let cidade = "Recife";      // tipo: string
cidade = 10;                // ❌ Erro: number não é string

// Arrays e tuplas
const notas: number[] = [8, 9.5, 7];
const coordenada: [number, number] = [-8.05, -34.9];

// União de tipos (union)
let id: string | number = "abc-123";

// Literais: o valor É o tipo
type Status = "pendente" | "pago" | "cancelado";
let statusPedido: Status = "pago";
statusPedido = "enviado";  // ❌ Erro: não faz parte da união
💡 Boa prática

Unions de literais ("pendente" | "pago") substituem enum na maior parte dos códigos modernos: são mais leves, não geram JavaScript extra e funcionam melhor com inferência.

2.4 Objetos: type vs interface

objetos.tsTypeScript
// interface: ideal para "formatos" de objetos, pode ser estendida
interface Usuario {
  id: string;
  nome: string;
  email: string;
  avatarUrl?: string;          // opcional
  readonly criadoEm: Date;     // não pode ser alterado
}

interface Admin extends Usuario {
  permissoes: string[];
}

// type: mais flexível — unions, tuplas, tipos calculados
type Resultado =
  | { sucesso: true; dados: Usuario }
  | { sucesso: false; erro: string };

function exibir(r: Resultado) {
  if (r.sucesso) {
    console.log(r.dados.nome);  // TS sabe que 'dados' existe aqui
  } else {
    console.log(r.erro);        // e que 'erro' existe aqui
  }
}
🎯 Pergunta clássica de entrevista

"Qual a diferença entre type e interface?" — Interfaces podem ser estendidas e sofrem declaration merging (duas declarações com o mesmo nome se fundem); types aceitam unions, interseções e tipos utilitários complexos. Na prática: use o que o time já usa; muitos padronizam type para tudo, reservando interface para contratos públicos de bibliotecas.

2.5 Funções tipadas

funcoes.tsTypeScript
// Parâmetros e retorno
function calcularDesconto(preco: number, percentual: number = 10): number {
  return preco * (1 - percentual / 100);
}

// Tipando callbacks
type Comparador<T> = (a: T, b: T) => number;

const porPreco: Comparador<{ preco: number }> = (a, b) => a.preco - b.preco;

// void e never
function logar(msg: string): void { console.log(msg); }
function falhar(msg: string): never { throw new Error(msg); }

2.6 Narrowing: o superpoder do dia a dia

Narrowing é o TypeScript "estreitando" um tipo amplo conforme você escreve condições. É o que torna unions seguras:

narrowing.tsTypeScript
function formatarId(id: string | number) {
  if (typeof id === "string") {
    return id.toUpperCase();   // aqui id é string
  }
  return id.toFixed(0);         // aqui id é number
}

// Discriminated union: o padrão mais importante do TS no mercado
type Evento =
  | { tipo: "click"; x: number; y: number }
  | { tipo: "tecla"; codigo: string }
  | { tipo: "scroll"; delta: number };

function tratar(e: Evento) {
  switch (e.tipo) {
    case "click":  return `(${e.x}, ${e.y})`;
    case "tecla":  return e.codigo;
    case "scroll": return e.delta;
    default: {
      // exhaustiveness check: se alguém adicionar um novo tipo
      // de Evento e esquecer de tratar, o TS acusa erro aqui
      const _nunca: never = e;
      return _nunca;
    }
  }
}

2.7 Generics: funções que preservam tipos

generics.tsTypeScript
// Sem generic: perde a informação do tipo
function primeiroRuim(arr: any[]): any { return arr[0]; }

// Com generic: o tipo "atravessa" a função
function primeiro<T>(arr: T[]): T | undefined {
  return arr[0];
}
const n = primeiro([1, 2, 3]);        // n: number | undefined
const s = primeiro(["a", "b"]);      // s: string | undefined

// Constraints: limitando o que T pode ser
function pegarProp<T, K extends keyof T>(obj: T, chave: K): T[K] {
  return obj[chave];
}
const u = { id: 1, nome: "Ana" };
pegarProp(u, "nome");   // string
pegarProp(u, "email");  // ❌ Erro: "email" não existe em u

// Generic em fetch tipado — você usará isso a apostila inteira
async function api<T>(url: string): Promise<T> {
  const res = await fetch(url);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json() as Promise<T>;
}

2.8 Utility types que caem em prova (e em produção)

UtilityO que fazExemplo
Partial<T>Todas as props opcionaisPayload de update
Required<T>Todas obrigatóriasConfig completa
Pick<T, K>Seleciona propriedadesPick<Usuario, "id" | "nome">
Omit<T, K>Remove propriedadesOmit<Usuario, "senha">
Record<K, V>Objeto chave→valorRecord<string, number>
ReturnType<F>Tipo de retorno de funçãoDerivar tipos de libs
Awaited<T>"Desembrulha" PromiseAwaited<ReturnType<typeof fn>>
utility.tsTypeScript
interface Tarefa {
  id: string;
  titulo: string;
  concluida: boolean;
  autorId: string;
}

// Criar: sem id (o banco gera)
type NovaTarefa = Omit<Tarefa, "id">;

// Atualizar: qualquer subconjunto, mas nunca o id
type AtualizarTarefa = Partial<Omit<Tarefa, "id">>;

// Resposta pública da API: só o essencial
type TarefaPublica = Pick<Tarefa, "id" | "titulo" | "concluida">;

2.9 O tsconfig que o mercado espera

tsconfig.jsonJSON
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,                    // inegociável em produção
    "noUncheckedIndexedAccess": true,  // arr[i] pode ser undefined
    "noImplicitOverride": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true
  }
}
⚠️ Sobre o any

any desliga o TypeScript. Em código profissional ele é praticamente proibido (times configuram ESLint para bloquear). Quando você realmente não sabe o tipo, use unknown e faça narrowing antes de usar — é a versão segura do any.

Exercícios do módulo 02
  1. Modele um type Pagamento como discriminated union com os tipos "pix" (com chave), "cartao" (com parcelas) e "boleto" (com vencimento). Escreva descreverPagamento(p) com exhaustiveness check.
  2. Implemente function agrupar<T, K extends keyof T>(itens: T[], chave: K): Record<string, T[]>.
  3. A partir de uma interface Produto com 6 campos, derive com utility types: NovoProduto, EditarProduto e ProdutoResumo.
  4. Reescreva a função api<T> para retornar { ok: true; data: T } | { ok: false; error: string } em vez de lançar exceção, e consuma-a com narrowing.
M03

React com TypeScript

Nível: básico → intermediário · Tempo sugerido: 2 semanas

React é a biblioteca de UI mais usada do mercado. Next.js é construído sobre ela — então antes de aprender o framework, você precisa dominar componentes, props, estado e hooks, tudo com tipos.

3.1 Componentes e JSX

Um componente é uma função que recebe props e retorna JSX (HTML dentro do JavaScript). Em TypeScript, os arquivos usam a extensão .tsx.

Botao.tsxTSX
type BotaoProps = {
  texto: string;
  variante?: "primario" | "secundario";
  onClick: () => void;
  children?: React.ReactNode;   // conteúdo entre as tags
};

export function Botao({ texto, variante = "primario", onClick }: BotaoProps) {
  return (
    <button
      className={variante === "primario" ? "btn-azul" : "btn-cinza"}
      onClick={onClick}
    >
      {texto}
    </button>
  );
}

// Uso:
// <Botao texto="Salvar" onClick={() => salvar()} />
💡 Regras do JSX
  • Retorne um único elemento raiz (use <>...</> — Fragment — se precisar agrupar).
  • Atributos em camelCase: className, onClick, htmlFor.
  • Expressões JavaScript entre chaves: {usuario.nome}.
  • Listas precisam de key estável (id do dado, nunca o índice quando a lista muda).

3.2 Estado com useState

Contador.tsxTSX
"use client";  // necessário no Next.js para componentes interativos (M05)

import { useState } from "react";

type Tarefa = { id: number; titulo: string; feita: boolean };

export function ListaDeTarefas() {
  const [tarefas, setTarefas] = useState<Tarefa[]>([]);
  const [titulo, setTitulo] = useState("");

  function adicionar() {
    if (!titulo.trim()) return;
    // Estado é imutável: sempre crie um NOVO array/objeto
    setTarefas((atual) => [
      ...atual,
      { id: Date.now(), titulo, feita: false },
    ]);
    setTitulo("");
  }

  function alternar(id: number) {
    setTarefas((atual) =>
      atual.map((t) => (t.id === id ? { ...t, feita: !t.feita } : t))
    );
  }

  return (
    <div>
      <input
        value={titulo}
        onChange={(e) => setTitulo(e.target.value)}
        placeholder="Nova tarefa"
      />
      <button onClick={adicionar}>Adicionar</button>

      <ul>
        {tarefas.map((t) => (
          <li key={t.id} onClick={() => alternar(t.id)}>
            {t.feita ? "✅" : "⬜"} {t.titulo}
          </li>
        ))}
      </ul>
    </div>
  );
}
⚠️ Os 3 erros de estado que reprovam em entrevista
  1. Mutar o estado: tarefas.push(...) não re-renderiza. Sempre crie novos arrays/objetos.
  2. Ler estado desatualizado: use a forma de função setX(atual => ...) quando o novo valor depende do anterior.
  3. Estado derivado duplicado: se dá para calcular a partir de outro estado (ex.: total de tarefas feitas), calcule na renderização em vez de criar outro useState.

3.3 Efeitos com useEffect — e quando NÃO usar

useEffect.tsxTSX
import { useEffect, useState } from "react";

export function Relogio() {
  const [hora, setHora] = useState(new Date());

  useEffect(() => {
    const id = setInterval(() => setHora(new Date()), 1000);
    return () => clearInterval(id);  // cleanup: SEMPRE limpe
  }, []);                              // [] = roda só ao montar

  return <p>{hora.toLocaleTimeString()}</p>;
}
🎯 Regra de ouro moderna

useEffect serve para sincronizar com sistemas externos (timers, WebSocket, APIs do navegador). Para buscar dados, o mercado migrou para Server Components (M05/M06) ou bibliotecas como TanStack Query — usar useEffect + fetch manualmente é considerado sinal de código legado.

3.4 Outros hooks essenciais

HookPara quêFrequência no mercado
useStateEstado localSempre
useEffectSincronizar com o mundo externoAlta (com cautela)
useRefReferência a DOM / valor mutável sem re-renderAlta
useContextCompartilhar dados sem prop drillingAlta
useMemo / useCallbackMemoizar cálculos/funções carasMédia (não abuse)
useReducerEstado complexo com açõesMédia

3.5 Composição: o padrão que separa júnior de pleno

Card.tsxTSX
// Em vez de um componente com 15 props booleanas...
// ...componha peças pequenas com children:

export function Card({ children }: { children: React.ReactNode }) {
  return <div className="card">{children}</div>;
}
Card.Header = function Header({ children }: { children: React.ReactNode }) {
  return <header className="card-header">{children}</header>;
};
Card.Body = function Body({ children }: { children: React.ReactNode }) {
  return <div className="card-body">{children}</div>;
};

// Uso flexível:
// <Card>
//   <Card.Header>Título</Card.Header>
//   <Card.Body>Qualquer conteúdo aqui</Card.Body>
// </Card>
💼 Visão de mercado

Vagas de front-end pedem, além de React: Tailwind CSS, componentes acessíveis (Radix UI / shadcn/ui), formulários com React Hook Form + Zod e estado de servidor com TanStack Query. Você verá Zod no M07 — ele é o mesmo dos formulários, o que torna a stack muito coesa.

Exercícios do módulo 03
  1. Crie um componente <Badge status="ativo" | "inativo" | "pendente" /> com cores diferentes por status, tipado com union de literais.
  2. Implemente um filtro na lista de tarefas: "todas | pendentes | concluídas", derivando a lista filtrada sem criar um novo estado.
  3. Construa um useLocalTimer(segundos) — hook customizado que faz contagem regressiva e retorna { restante, pausar, retomar }.
  4. Refatore um componente <Modal title footer size showClose ... /> cheio de props para o padrão de composição.
M04

Next.js e o App Router

Nível: intermediário · Tempo sugerido: 2 semanas

Next.js é um framework full stack sobre React: roteamento por arquivos, renderização no servidor, API integrada, otimizações automáticas. É o padrão de fato para React em produção.

4.1 Criando o projeto (início do DevBoard)

terminalbash
npx create-next-app@latest devboard
# Responda:
# ✔ TypeScript? ........ Yes
# ✔ ESLint? ............ Yes
# ✔ Tailwind CSS? ...... Yes
# ✔ src/ directory? .... Yes
# ✔ App Router? ........ Yes  ← fundamental
# ✔ Import alias @/* ... Yes

cd devboard && npm run dev   # http://localhost:3000

4.2 Roteamento por sistema de arquivos

No App Router, pastas definem rotas e arquivos especiais definem a UI:

estrutura do projetoárvore
src/app/
├── layout.tsx          # layout raiz (html, body) — obrigatório
├── page.tsx            # rota /
├── globals.css
├── dashboard/
│   ├── layout.tsx      # layout compartilhado do dashboard
│   ├── page.tsx        # rota /dashboard
│   ├── loading.tsx     # UI de carregamento automática (Suspense)
│   ├── error.tsx       # captura erros dessa subárvore
│   └── projetos/
│       ├── page.tsx            # /dashboard/projetos
│       └── [id]/
│           └── page.tsx        # /dashboard/projetos/42 (dinâmica)
├── (marketing)/        # route group: organiza sem afetar a URL
│   ├── sobre/page.tsx          # /sobre
│   └── precos/page.tsx         # /precos
└── api/                # Route Handlers (M07)
    └── tarefas/route.ts        # /api/tarefas
ArquivoFunção
page.tsxUI da rota (torna a pasta acessível por URL)
layout.tsxUI compartilhada que envolve as páginas filhas; preserva estado ao navegar
loading.tsxFallback de carregamento (Suspense automático)
error.tsxError boundary da subárvore (precisa ser Client Component)
not-found.tsxUI para 404 / notFound()
route.tsEndpoint de API (GET, POST...)

4.3 Páginas, layouts e navegação

src/app/dashboard/layout.tsxTSX
import Link from "next/link";

export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div className="flex min-h-screen">
      <aside className="w-64 border-r p-4">
        <nav className="flex flex-col gap-2">
          <Link href="/dashboard">Visão geral</Link>
          <Link href="/dashboard/projetos">Projetos</Link>
        </nav>
      </aside>
      <main className="flex-1 p-6">{children}</main>
    </div>
  );
}
src/app/dashboard/projetos/[id]/page.tsxTSX
import { notFound } from "next/navigation";

// Em Next 15, params é uma Promise — sempre use await
type Props = { params: Promise<{ id: string }> };

export default async function ProjetoPage({ params }: Props) {
  const { id } = await params;
  const projeto = await buscarProjeto(id);   // função sua (M06)

  if (!projeto) notFound();   // renderiza o not-found.tsx

  return <h1 className="text-2xl font-bold">{projeto.nome}</h1>;
}
💡 Navegação

Use <Link> para links (com prefetch automático) e o hook useRouter() de next/navigation para navegar programaticamente (router.push("/dashboard")). Nunca importe de next/router — esse é o pacote do Pages Router antigo.

4.4 Estratégias de renderização (tema favorito de entrevista)

EstratégiaQuando o HTML é geradoIdeal para
SSG (estática)No buildBlog, docs, landing pages
ISR (incremental)No build + revalidação periódicaE-commerce, catálogos
SSR (dinâmica)A cada requisiçãoDashboards, dados por usuário
CSR (cliente)No navegadorPartes altamente interativas

No App Router você não escolhe com funções especiais (getStaticProps etc. são do modelo antigo): a estratégia emerge de como você busca dados e configura cache — detalhado no M06.

4.5 Metadados e SEO

src/app/(marketing)/precos/page.tsxTSX
import type { Metadata } from "next";

export const metadata: Metadata = {
  title: "Preços | DevBoard",
  description: "Planos flexíveis para times de todos os tamanhos.",
  openGraph: { title: "DevBoard", images: ["/og.png"] },
};

// Metadados dinâmicos:
// export async function generateMetadata({ params }): Promise<Metadata> { ... }
💼 Visão de mercado

Saber explicar SSR vs SSG vs ISR e por que isso importa para SEO e performance é pergunta quase garantida em entrevistas de Next.js. Prepare uma resposta com exemplo concreto: "página de produto com ISR de 60s: rápida como estática, atualiza preço sem rebuild".

Exercícios do módulo 04
  1. Monte o esqueleto do DevBoard: landing page em (marketing), área /dashboard com layout de sidebar e rota dinâmica /dashboard/projetos/[id].
  2. Adicione loading.tsx e error.tsx no dashboard; simule lentidão com await new Promise(r => setTimeout(r, 2000)) e um erro proposital para ver ambos funcionando.
  3. Implemente generateMetadata na página de projeto usando o nome do projeto no título.
M05

Server Components e Client Components

Nível: intermediário · Tempo sugerido: 1 semana

Este é o conceito que define o Next.js moderno — e o que mais derruba candidatos. Entenda isso bem e você estará à frente da maioria.

5.1 O modelo mental

No App Router, todo componente é Server Component por padrão: ele roda no servidor, pode acessar banco de dados diretamente e envia ao navegador apenas HTML + uma descrição serializada da árvore (o RSC payload) — sem o JavaScript do componente.

Quando você precisa de interatividade (estado, eventos, APIs do navegador), marca o arquivo com "use client". Esse componente e tudo que ele importa passam a ser enviados como JavaScript para o navegador.

CapacidadeServer ComponentClient Component
Acessar banco/segredos diretamente
Ser async e usar await no corpo
useState, useEffect, eventos
APIs do navegador (localStorage, window)
Peso no bundle do clienteZeroConta no bundle

5.2 Combinando os dois — o padrão certo

src/app/dashboard/projetos/page.tsx (Server)TSX
import { db } from "@/lib/db";
import { FiltroProjetos } from "./filtro";

// Server Component: async, busca dados direto, zero JS no cliente
export default async function ProjetosPage() {
  const projetos = await db.projeto.findMany();

  return (
    <section>
      <h1>Projetos</h1>
      {/* passa dados do servidor para o componente interativo */}
      <FiltroProjetos projetos={projetos} />
    </section>
  );
}
src/app/dashboard/projetos/filtro.tsx (Client)TSX
"use client";

import { useState } from "react";
import type { Projeto } from "@prisma/client";

export function FiltroProjetos({ projetos }: { projetos: Projeto[] }) {
  const [busca, setBusca] = useState("");

  const filtrados = projetos.filter((p) =>
    p.nome.toLowerCase().includes(busca.toLowerCase())
  );

  return (
    <>
      <input value={busca} onChange={(e) => setBusca(e.target.value)} />
      <ul>{filtrados.map((p) => <li key={p.id}>{p.nome}</li>)}</ul>
    </>
  );
}

5.3 Regras que evitam 90% dos bugs

  • Empurre o "use client" para as folhas: mantenha páginas e layouts como Server Components; isole a interatividade em componentes pequenos.
  • Server pode renderizar Client, mas Client não importa Server. Para colocar conteúdo de servidor "dentro" de um Client Component, passe como children.
  • Props precisam ser serializáveis: você não pode passar funções, Date vira string, classes não atravessam a fronteira.
  • Segredos ficam no servidor: variáveis de ambiente sem prefixo NEXT_PUBLIC_ nunca chegam ao navegador. Use o pacote server-only para garantir que módulos sensíveis não vazem.
padrão childrenTSX
// ✅ Client Component que "abraça" conteúdo de servidor
"use client";
export function Accordion({ children }: { children: React.ReactNode }) {
  const [aberto, setAberto] = useState(false);
  return (
    <div>
      <button onClick={() => setAberto(!aberto)}>Detalhes</button>
      {aberto && children}   {/* children veio pronto do servidor */}
    </div>
  );
}
⚠️ Mitos comuns
  • "use client" não significa "renderiza só no navegador" — Client Components também são pré-renderizados no servidor (SSR) e depois "hidratados".
  • Server Components não substituem API: se um app mobile ou terceiros precisam dos dados, você ainda expõe endpoints (M07).
Exercícios do módulo 05
  1. No DevBoard, crie a página de projetos como Server Component com dados mockados e um componente cliente de busca, como no exemplo.
  2. Tente usar useState num Server Component e importar um módulo com import "server-only" num Client Component. Leia e anote as mensagens de erro — você vai revê-las na vida real.
  3. Implemente um <ThemeToggle /> (dark mode) como Client Component minúsculo dentro de um layout 100% server.
M06

Data fetching, cache e Server Actions

Nível: intermediário → avançado · Tempo sugerido: 2 semanas

Aqui o Next.js vira full stack de verdade: buscar dados no servidor, controlar cache com precisão e mutar dados sem escrever endpoints manuais.

6.1 Buscando dados em Server Components

data fetchingTSX
// Com fetch (APIs externas)
export default async function Page() {
  const res = await fetch("https://api.exemplo.com/posts", {
    next: { revalidate: 60 },        // ISR: revalida a cada 60s
    // cache: "force-cache"  → estático
    // cache: "no-store"     → sempre dinâmico (SSR puro)
  });
  const posts: Post[] = await res.json();
  return <Lista posts={posts} />;
}

// Direto no banco (sem fetch) — cache controlado na rota:
export const revalidate = 60;          // ISR para a página inteira
// export const dynamic = "force-dynamic";  // SSR sempre
🎯 Como pensar cache no Next 15

Requisições fetch não são mais cacheadas por padrão (mudança em relação ao Next 14 — pegadinha de entrevista!). Você ativa cache explicitamente com cache: "force-cache" ou next.revalidate. Rotas continuam sendo estáticas por padrão quando não usam dados dinâmicos (cookies(), headers(), searchParams).

6.2 Streaming com Suspense

Em vez de esperar a página inteira, envie o que está pronto e "streame" o resto:

streamingTSX
import { Suspense } from "react";

export default function Dashboard() {
  return (
    <>
      <h1>Dashboard</h1>                        {/* aparece na hora */}
      <Suspense fallback={<SkeletonCards />}>
        <MetricasLentas />                       {/* streama quando pronto */}
      </Suspense>
      <Suspense fallback={<SkeletonLista />}>
        <AtividadesRecentes />
      </Suspense>
    </>
  );
}

6.3 Server Actions: mutações sem endpoint

Server Actions são funções que rodam no servidor mas podem ser chamadas de formulários e componentes como se fossem locais. O Next.js cria o endpoint por baixo dos panos.

src/app/dashboard/tarefas/actions.tsTypeScript
"use server";

import { z } from "zod";
import { db } from "@/lib/db";
import { revalidatePath } from "next/cache";

const schema = z.object({
  titulo: z.string().min(3, "Título muito curto"),
  projetoId: z.string().cuid(),
});

export type ActionState = { ok: boolean; message: string };

export async function criarTarefa(
  _prev: ActionState,
  formData: FormData
): Promise<ActionState> {
  // 1. NUNCA confie no cliente: valide sempre no servidor
  const parsed = schema.safeParse(Object.fromEntries(formData));
  if (!parsed.success) {
    return { ok: false, message: parsed.error.issues[0].message };
  }

  // 2. (M09) Verifique autenticação/autorização aqui!

  await db.tarefa.create({ data: parsed.data });

  // 3. Invalida o cache da página para refletir o novo dado
  revalidatePath("/dashboard/tarefas");
  return { ok: true, message: "Tarefa criada!" };
}
src/app/dashboard/tarefas/form.tsxTSX
"use client";

import { useActionState } from "react";
import { criarTarefa, type ActionState } from "./actions";

const inicial: ActionState = { ok: true, message: "" };

export function NovaTarefaForm({ projetoId }: { projetoId: string }) {
  const [state, action, pending] = useActionState(criarTarefa, inicial);

  return (
    <form action={action} className="flex gap-2">
      <input name="titulo" placeholder="Nova tarefa" required />
      <input type="hidden" name="projetoId" value={projetoId} />
      <button disabled={pending}>
        {pending ? "Salvando..." : "Criar"}
      </button>
      {!state.ok && <p className="text-red-600">{state.message}</p>}
    </form>
  );
}

6.4 Revalidação e UI otimista

  • revalidatePath("/rota") — invalida o cache de uma rota após mutação.
  • revalidateTag("tarefas") — invalida qualquer fetch marcado com next: { tags: ['tarefas'] }; mais cirúrgico em apps grandes.
  • useOptimistic — mostra o resultado imediatamente enquanto a action roda, revertendo em caso de erro; padrão esperado em produtos com boa UX.
💼 Visão de mercado

O trio Server Actions + Zod + revalidate substituiu, em muitos times, dezenas de endpoints CRUD. Em entrevistas de nível pleno, espere perguntas como: "Server Action ou Route Handler — quando usar cada um?" Resposta curta: Actions para mutações do próprio app (forms internos); Route Handlers para APIs consumidas por terceiros, webhooks e mobile.

Exercícios do módulo 06
  1. Implemente o CRUD de tarefas do DevBoard com Server Actions (criar, alternar concluída, excluir), validando com Zod e revalidando a rota.
  2. Adicione useOptimistic ao alternar o checkbox de "concluída".
  3. Divida o dashboard em 2 blocos com Suspense e delays artificiais diferentes; observe o streaming na aba Network.
  4. Marque um fetch com tag e crie um botão "Atualizar dados" que chama uma action com revalidateTag.
M07

API com Route Handlers e validação

Nível: intermediário → avançado · Tempo sugerido: 1–2 semanas

Nem tudo é Server Action: apps mobile, integrações e webhooks precisam de uma API HTTP de verdade. No Next.js, isso é feito com Route Handlers — endpoints tipados dentro do próprio projeto.

7.1 Anatomia de um Route Handler

src/app/api/tarefas/route.tsTypeScript
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { db } from "@/lib/db";

// GET /api/tarefas?status=pendente&page=1
export async function GET(req: NextRequest) {
  const status = req.nextUrl.searchParams.get("status");
  const page = Number(req.nextUrl.searchParams.get("page") ?? 1);

  const tarefas = await db.tarefa.findMany({
    where: status ? { status } : undefined,
    take: 20,
    skip: (page - 1) * 20,
    orderBy: { criadoEm: "desc" },
  });

  return NextResponse.json({ data: tarefas, page });
}

const criarSchema = z.object({
  titulo: z.string().min(3).max(120),
  projetoId: z.string().cuid(),
});

// POST /api/tarefas
export async function POST(req: NextRequest) {
  const body = await req.json().catch(() => null);
  const parsed = criarSchema.safeParse(body);

  if (!parsed.success) {
    return NextResponse.json(
      { error: "Dados inválidos", issues: parsed.error.flatten() },
      { status: 422 }
    );
  }

  const tarefa = await db.tarefa.create({ data: parsed.data });
  return NextResponse.json(tarefa, { status: 201 });
}

Rotas dinâmicas seguem o mesmo padrão de pastas: src/app/api/tarefas/[id]/route.ts exporta GET, PATCH e DELETE recebendo { params } (também uma Promise no Next 15).

7.2 Zod: a fronteira entre o caos e o seu código

Todo dado que entra pela rede é unknown. O Zod valida em runtime e gera o tipo TypeScript de graça:

schemas.tsTypeScript
import { z } from "zod";

export const usuarioSchema = z.object({
  nome: z.string().min(2),
  email: z.string().email(),
  idade: z.coerce.number().int().min(18).optional(),
  papel: z.enum(["admin", "membro"]).default("membro"),
});

// Um schema, um tipo: fonte única de verdade
export type Usuario = z.infer<typeof usuarioSchema>;
// { nome: string; email: string; idade?: number; papel: "admin" | "membro" }

// Também valide variáveis de ambiente na inicialização:
export const env = z.object({
  DATABASE_URL: z.string().url(),
  AUTH_SECRET: z.string().min(32),
}).parse(process.env);
💡 Padrão profissional

Coloque os schemas em src/lib/schemas/ e reutilize o mesmo schema no formulário (React Hook Form + zodResolver), na Server Action e no Route Handler. Validação duplicada e divergente é fonte clássica de bug em produção.

7.3 Padrões de API que entrevistadores procuram

  • REST consistente: substantivos no plural (/api/projetos/:id/tarefas), verbos pelos métodos HTTP, status codes corretos.
  • Formato de erro previsível: sempre { error, issues? } — o front agradece.
  • Paginação: page/limit (simples) ou cursor (escala melhor; padrão em feeds).
  • Rate limiting em rotas públicas (ex.: Upstash Ratelimit) e verificação de assinatura em webhooks (Stripe, GitHub).
  • Nunca vaze campos sensíveis: selecione colunas explicitamente; jamais retorne hash de senha.
Exercícios do módulo 07
  1. Implemente a API pública do DevBoard: GET/POST /api/tarefas e GET/PATCH/DELETE /api/tarefas/[id], com Zod e status codes corretos.
  2. Adicione paginação por cursor em GET /api/tarefas e documente o contrato num README.
  3. Crie um handler POST /api/webhooks/exemplo que rejeita requisições sem um header x-signature válido.
  4. Teste tudo com a extensão REST Client do VS Code ou com curl, incluindo os casos de erro.
M08

Banco de dados com Prisma

Nível: intermediário → avançado · Tempo sugerido: 2 semanas

Prisma é o ORM mais popular do ecossistema TypeScript: você descreve o schema, ele gera migrações SQL e um cliente 100% tipado. Vamos usá-lo com PostgreSQL, o banco relacional padrão do mercado.

8.1 Setup

terminalbash
npm install prisma -D
npm install @prisma/client
npx prisma init --datasource-provider postgresql

# Postgres local rápido com Docker:
docker run --name pg-devboard -e POSTGRES_PASSWORD=dev \
  -e POSTGRES_DB=devboard -p 5432:5432 -d postgres:16

# .env
# DATABASE_URL="postgresql://postgres:dev@localhost:5432/devboard"

8.2 Modelando o DevBoard

prisma/schema.prismaPrisma
model User {
  id        String   @id @default(cuid())
  email     String   @unique
  nome      String
  senhaHash String?
  papel     Papel    @default(MEMBRO)
  projetos  Projeto[]
  tarefas   Tarefa[]
  criadoEm  DateTime @default(now())
}

model Projeto {
  id       String  @id @default(cuid())
  nome     String
  dono     User    @relation(fields: [donoId], references: [id])
  donoId   String
  tarefas  Tarefa[]

  @@index([donoId])
}

model Tarefa {
  id          String   @id @default(cuid())
  titulo      String
  status      Status   @default(PENDENTE)
  projeto     Projeto  @relation(fields: [projetoId], references: [id], onDelete: Cascade)
  projetoId   String
  responsavel User?    @relation(fields: [userId], references: [id])
  userId      String?
  criadoEm    DateTime @default(now())

  @@index([projetoId, status])
}

enum Papel  { ADMIN MEMBRO }
enum Status { PENDENTE EM_ANDAMENTO CONCLUIDA }
terminalbash
npx prisma migrate dev --name init   # gera e aplica a migração SQL
npx prisma studio                    # GUI para inspecionar os dados

8.3 O cliente singleton (obrigatório no Next.js)

src/lib/db.tsTypeScript
import { PrismaClient } from "@prisma/client";

// Em dev, o hot reload recriaria clientes infinitamente
// e esgotaria as conexões do banco. Este padrão evita isso.
const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient };

export const db = globalForPrisma.prisma ?? new PrismaClient();

if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = db;

8.4 Consultas do dia a dia (tudo tipado)

queries.tsTypeScript
// Relacionamentos: include vs select
const projeto = await db.projeto.findUnique({
  where: { id },
  select: {
    id: true, nome: true,
    tarefas: { where: { status: "PENDENTE" }, orderBy: { criadoEm: "desc" } },
    _count: { select: { tarefas: true } },
  },
});
// O tipo de retorno reflete EXATAMENTE o select — autocomplete total.

// Agregações
const porStatus = await db.tarefa.groupBy({
  by: ["status"],
  _count: { _all: true },
  where: { projetoId },
});

// Transações: tudo ou nada
await db.$transaction(async (tx) => {
  const p = await tx.projeto.create({ data: { nome, donoId } });
  await tx.tarefa.createMany({
    data: modelos.map((t) => ({ ...t, projetoId: p.id })),
  });
});

// Upsert: cria ou atualiza
await db.user.upsert({
  where: { email },
  update: { nome },
  create: { email, nome },
});

8.5 O que separa você da média

  • Problema N+1: buscar uma lista e depois fazer uma query por item. Solução: include/select na query original. Saiba explicar isso — é pergunta recorrente.
  • Índices: crie @@index para colunas usadas em where e orderBy; entenda que @unique já cria índice.
  • Migrações em produção: prisma migrate deploy no CI, nunca migrate dev.
  • Connection pooling em serverless: em plataformas como Vercel, cada função abre conexões; use um pooler (PgBouncer, Prisma Accelerate, ou o pooler do Neon/Supabase).
  • SQL puro por baixo: saiba escrever JOIN, GROUP BY e ler um EXPLAIN. ORMs mudam; SQL é para sempre.
💼 Visão de mercado

Bancos gerenciados dominam as vagas: Neon e Supabase (Postgres serverless) e PlanetScale (MySQL). O concorrente direto do Prisma é o Drizzle ORM — mais leve e próximo do SQL; vale conhecer os dois nomes para conversar em entrevista.

Exercícios do módulo 08
  1. Implemente o schema do DevBoard, rode a migração e crie um prisma/seed.ts que popula 2 usuários, 3 projetos e 15 tarefas.
  2. Escreva a query do dashboard: projetos do usuário com contagem de tarefas por status, em uma ida ao banco.
  3. Provoque um N+1 de propósito (loop com findUnique), meça com console.time, depois corrija.
  4. Adicione um campo prioridade ao modelo, gere a migração e ajuste o código que quebrar (o TS vai te guiar).
M09

Autenticação e autorização

Nível: avançado · Tempo sugerido: 2 semanas

Login é a funcionalidade mais sensível de qualquer app — e um dos temas mais cobrados em entrevistas de full stack. Vamos entender os conceitos e implementar com Auth.js (NextAuth v5).

9.1 Conceitos que você PRECISA saber explicar

ConceitoEm uma frase
AutenticaçãoProvar quem você é (login).
AutorizaçãoO que você pode fazer (permissões, papéis).
Sessão (stateful)Servidor guarda a sessão; navegador guarda só um ID em cookie.
JWT (stateless)Token assinado carrega os dados; servidor só verifica a assinatura.
OAuth"Entrar com Google/GitHub": delega a autenticação a um provedor.
Hash de senhaSenhas nunca são salvas em texto; use bcrypt ou argon2.
⚠️ Segurança de cookies

Cookies de sessão devem ser HttpOnly (JS não lê — mitiga XSS), Secure (só HTTPS) e SameSite=Lax (mitiga CSRF). Guardar token de autenticação em localStorage é considerado má prática — qualquer XSS rouba o token.

9.2 Auth.js (NextAuth v5) no DevBoard

terminalbash
npm install next-auth@beta @auth/prisma-adapter bcryptjs
npx auth secret   # gera AUTH_SECRET no .env.local
src/auth.tsTypeScript
import NextAuth from "next-auth";
import GitHub from "next-auth/providers/github";
import Credentials from "next-auth/providers/credentials";
import { PrismaAdapter } from "@auth/prisma-adapter";
import { compare } from "bcryptjs";
import { db } from "@/lib/db";
import { z } from "zod";

const loginSchema = z.object({
  email: z.string().email(),
  senha: z.string().min(8),
});

export const { handlers, auth, signIn, signOut } = NextAuth({
  adapter: PrismaAdapter(db),
  session: { strategy: "jwt" },
  providers: [
    GitHub,   // AUTH_GITHUB_ID e AUTH_GITHUB_SECRET no .env
    Credentials({
      credentials: { email: {}, senha: {} },
      async authorize(creds) {
        const parsed = loginSchema.safeParse(creds);
        if (!parsed.success) return null;

        const user = await db.user.findUnique({
          where: { email: parsed.data.email },
        });
        if (!user?.senhaHash) return null;

        const ok = await compare(parsed.data.senha, user.senhaHash);
        return ok ? { id: user.id, email: user.email, name: user.nome } : null;
      },
    }),
  ],
  callbacks: {
    jwt({ token, user }) {
      if (user) token.id = user.id;   // enriquece o token no login
      return token;
    },
    session({ session, token }) {
      session.user.id = token.id as string;
      return session;
    },
  },
});
src/app/api/auth/[...nextauth]/route.tsTypeScript
import { handlers } from "@/auth";
export const { GET, POST } = handlers;

9.3 Protegendo páginas, actions e API

proteção em camadasTypeScript
// 1) Página (Server Component)
import { auth } from "@/auth";
import { redirect } from "next/navigation";

export default async function DashboardPage() {
  const session = await auth();
  if (!session?.user) redirect("/login");
  return <h1>Olá, {session.user.name}</h1>;
}

// 2) Server Action — verifique DE NOVO (defesa em profundidade)
"use server";
export async function excluirTarefa(id: string) {
  const session = await auth();
  if (!session?.user) throw new Error("Não autenticado");

  // Autorização: o recurso pertence ao usuário?
  const tarefa = await db.tarefa.findUnique({
    where: { id },
    select: { projeto: { select: { donoId: true } } },
  });
  if (tarefa?.projeto.donoId !== session.user.id) {
    throw new Error("Sem permissão");   // evita IDOR
  }
  await db.tarefa.delete({ where: { id } });
}

// 3) Middleware — proteção de rota em larga escala
// middleware.ts na raiz: redireciona /dashboard/* sem sessão.
// Importante: middleware é UX, não segurança final —
// a checagem de verdade fica na página/action/handler.

9.4 Vulnerabilidades que caem em entrevista

AtaqueO que éDefesa
XSSInjetar script na páginaReact escapa por padrão; evite dangerouslySetInnerHTML; sanitize HTML externo
SQL InjectionInjetar SQL via inputORM/queries parametrizadas (Prisma já protege)
CSRFSite malicioso dispara ações autenticadasSameSite cookies; Server Actions têm proteção embutida
IDORAcessar recurso alheio trocando o ID na URLSempre checar dono/permissão no servidor
Brute forceTentar milhares de senhasRate limiting + hash lento (bcrypt/argon2)
Exercícios do módulo 09
  1. Implemente cadastro (hash com bcrypt), login com credenciais e login com GitHub no DevBoard.
  2. Proteja todas as Server Actions do CRUD com autenticação e verificação de posse do recurso.
  3. Crie o papel ADMIN com uma página /admin que lista todos os usuários — bloqueada para membros comuns.
  4. Escreva, com suas palavras, a diferença entre sessão em banco e JWT, e quando escolheria cada um. Treine falar em voz alta (2 minutos).
M10

TypeScript avançado

Nível: avançado → muito avançado · Tempo sugerido: 2–3 semanas

O sistema de tipos do TypeScript é uma linguagem funcional completa. Dominá-lo permite criar APIs internas impossíveis de usar errado — a marca de um desenvolvedor sênior.

10.1 keyof, typeof e indexed access

operadores.tsTypeScript
const config = {
  api: { url: "https://api.dev", timeout: 5000 },
  tema: { primario: "#3178C6" },
} as const;   // congela: tipos viram literais readonly

type Config = typeof config;            // tipo a partir do valor
type Secao = keyof Config;              // "api" | "tema"
type Timeout = Config["api"]["timeout"]; // 5000 (literal!)

// Padrão de mercado: derivar unions de arrays com as const
const STATUS = ["pendente", "em_andamento", "concluida"] as const;
type Status = (typeof STATUS)[number];  // "pendente" | "em_andamento" | "concluida"

10.2 Conditional types e infer

conditional.tsTypeScript
// Sintaxe: A extends B ? X : Y  — um if/else de tipos
type EhString<T> = T extends string ? true : false;

// infer: captura um tipo "de dentro" de outro
type ElementoDe<T> = T extends (infer U)[] ? U : never;
type A = ElementoDe<string[]>;        // string

// Reimplementando utilities nativas (clássico de entrevista):
type MeuReturnType<T> =
  T extends (...args: any[]) => infer R ? R : never;

type MeuAwaited<T> =
  T extends Promise<infer U> ? MeuAwaited<U> : T;  // recursivo!

// Distribuição sobre unions: acontece automaticamente
type SemNulos<T> = T extends null | undefined ? never : T;
type B = SemNulos<string | null | number>;   // string | number

10.3 Mapped types e template literal types

mapped.tsTypeScript
// Mapped type: transforma cada propriedade de um tipo
type Opcional<T> = { [K in keyof T]?: T[K] };          // = Partial
type SomenteLeitura<T> = { readonly [K in keyof T]: T[K] };

// Template literals: strings com estrutura tipada
type Rota = `/api/${string}`;
const ok: Rota = "/api/tarefas";   // ✅
const ruim: Rota = "/tarefas";     // ❌

// Combinando os dois: gerar getters automaticamente
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type UserGetters = Getters<{ nome: string; idade: number }>;
// { getNome: () => string; getIdade: () => number }

// Extraindo params de rota — como o Next.js tipa rotas dinâmicas
type Params<T extends string> =
  T extends `${string}/[${infer P}]/${infer Resto}`
    ? { [K in P]: string } & Params<`/${Resto}`>
    : T extends `${string}/[${infer P}]`
      ? { [K in P]: string }
      : {};
type R = Params<"/projetos/[id]/tarefas/[tarefaId]">;
// { id: string } & { tarefaId: string }

10.4 Type guards, satisfies e branded types

avancado.tsTypeScript
// Type guard customizado: função que ensina o compilador
function ehUsuario(v: unknown): v is { id: string; email: string } {
  return (
    typeof v === "object" && v !== null &&
    "id" in v && "email" in v
  );
}

// satisfies: valida o formato SEM alargar o tipo
const paleta = {
  primario: "#3178C6",
  erro: [180, 35, 24],
} satisfies Record<string, string | number[]>;

paleta.primario.toUpperCase();  // ✅ TS sabe que é string
paleta.erro.map((n) => n);      // ✅ TS sabe que é number[]
// Com ": Record<...>" em vez de satisfies, os dois virariam
// "string | number[]" e você perderia os métodos. Pegadinha clássica!

// Branded types: IDs que não se misturam
type UserId = string & { readonly __brand: "UserId" };
type ProjetoId = string & { readonly __brand: "ProjetoId" };

function buscarUsuario(id: UserId) { /* ... */ }
declare const projetoId: ProjetoId;
buscarUsuario(projetoId);   // ❌ Erro: brands diferentes!
// Em runtime são strings normais; a proteção existe só em compile time.

10.5 Padrão Result: erros como valores

result.tsTypeScript
export type Result<T, E = string> =
  | { ok: true; value: T }
  | { ok: false; error: E };

export const Ok = <T>(value: T): Result<T, never> => ({ ok: true, value });
export const Err = <E>(error: E): Result<never, E> => ({ ok: false, error });

// Uso: quem chama é OBRIGADO a tratar o erro
async function cobrar(valor: number): Promise<Result<{ id: string }, "saldo" | "rede">> {
  if (valor > 1000) return Err("saldo");
  return Ok({ id: "tx_123" });
}

const r = await cobrar(500);
if (!r.ok) {
  // r.error é "saldo" | "rede" — trate cada caso
} else {
  r.value.id;   // seguro
}
💼 Visão de mercado

Para treinar tipos avançados no formato que aparece em entrevistas técnicas, resolva os desafios do repositório type-challenges no GitHub (níveis easy → medium cobrem 95% do que se pede). Citar que você pratica lá é um ótimo sinal em entrevista.

Exercícios do módulo 10
  1. Implemente do zero: MeuPick<T, K>, MeuOmit<T, K> e DeepPartial<T> (recursivo).
  2. Crie type QueryString<T> que converte { page: number; q: string } em "page=${number}&q=${string}" com template literals.
  3. Adicione branded types UserId e ProjetoId ao DevBoard com funções construtoras validadas por Zod.
  4. Refatore uma Server Action do DevBoard para retornar Result com união de erros literais em vez de lançar exceções.
M11

Testes automatizados

Nível: avançado · Tempo sugerido: 2 semanas

"Você escreve testes?" é pergunta eliminatória em processos para pleno. A boa notícia: a pirâmide de testes moderna em Next.js é bem definida — Vitest para unidade, Testing Library para componentes, Playwright para ponta a ponta.

11.1 A pirâmide na prática

CamadaFerramentaTesta o quêQuantidade
UnitárioVitestFunções puras, schemas, utilsMuitos
ComponenteVitest + Testing LibraryComportamento da UIMédio
E2EPlaywrightFluxos críticos no navegador realPoucos

11.2 Vitest: testes unitários

terminalbash
npm install -D vitest @testing-library/react @testing-library/user-event \
  @testing-library/jest-dom jsdom @vitejs/plugin-react
src/lib/preco.test.tsTypeScript
import { describe, it, expect } from "vitest";
import { calcularDesconto } from "./preco";

describe("calcularDesconto", () => {
  it("aplica o percentual sobre o preço", () => {
    expect(calcularDesconto(100, 10)).toBe(90);
  });

  it("lança erro para percentual inválido", () => {
    expect(() => calcularDesconto(100, 150)).toThrow();
  });

  // it.each: tabela de casos — impressiona em code review
  it.each([
    [200, 0, 200],
    [200, 50, 100],
    [200, 100, 0],
  ])("preço %i com %i%% => %i", (preco, pct, esperado) => {
    expect(calcularDesconto(preco, pct)).toBe(esperado);
  });
});

11.3 Testando componentes: comportamento, não implementação

form.test.tsxTSX
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it, expect, vi } from "vitest";
import { NovaTarefaForm } from "./form";

describe("NovaTarefaForm", () => {
  it("envia o título digitado", async () => {
    const onSubmit = vi.fn();
    render(<NovaTarefaForm onSubmit={onSubmit} />);

    // Busque como o USUÁRIO enxerga: por papel e rótulo acessível
    await userEvent.type(
      screen.getByRole("textbox", { name: /título/i }),
      "Estudar Playwright"
    );
    await userEvent.click(screen.getByRole("button", { name: /criar/i }));

    expect(onSubmit).toHaveBeenCalledWith("Estudar Playwright");
  });

  it("mostra erro para título curto", async () => {
    render(<NovaTarefaForm onSubmit={vi.fn()} />);
    await userEvent.click(screen.getByRole("button", { name: /criar/i }));
    expect(await screen.findByText(/muito curto/i)).toBeInTheDocument();
  });
});
💡 Filosofia da Testing Library

"Quanto mais seus testes se parecem com o modo como o software é usado, mais confiança eles dão." Prefira getByRole e getByLabelText; evite testar estado interno ou classes CSS — de quebra, isso força você a escrever HTML acessível.

11.4 Playwright: E2E dos fluxos críticos

e2e/login.spec.tsTypeScript
import { test, expect } from "@playwright/test";

test("usuário faz login e cria uma tarefa", async ({ page }) => {
  await page.goto("/login");
  await page.getByLabel("E-mail").fill("ana@teste.com");
  await page.getByLabel("Senha").fill("senha-segura-123");
  await page.getByRole("button", { name: "Entrar" }).click();

  await expect(page).toHaveURL("/dashboard");

  await page.getByPlaceholder("Nova tarefa").fill("Revisar PR");
  await page.getByRole("button", { name: "Criar" }).click();

  await expect(page.getByText("Revisar PR")).toBeVisible();
});
  • Rode E2E contra um banco de teste resetado por seed a cada execução.
  • Teste E2E apenas os fluxos que quebram o negócio: login, cadastro, checkout/criação principal.
  • Server Components e Actions são melhor cobertos por E2E ou testes de integração do que por testes unitários de componente.
Exercícios do módulo 11
  1. Cubra seus schemas Zod do DevBoard com testes unitários (casos válidos e inválidos com it.each).
  2. Teste o componente de filtro de projetos (M05): digitar filtra a lista, campo vazio mostra tudo.
  3. Escreva o E2E "cadastro → login → criar projeto → criar tarefa → marcar concluída" com Playwright.
  4. Configure npm run test e npm run test:e2e como scripts separados — você os usará no CI (M14).
M12

Arquitetura e padrões de projeto

Nível: muito avançado · Tempo sugerido: 2 semanas

Código que funciona é o mínimo. O que diferencia níveis de senioridade é organizar código que continua fácil de mudar depois de 2 anos e 20 desenvolvedores.

12.1 Estrutura de pastas que escala

organização por featureárvore
src/
├── app/                    # SÓ roteamento: pages, layouts, handlers finos
├── features/               # o coração do app, por domínio
│   ├── tarefas/
│   │   ├── components/     # UI específica de tarefas
│   │   ├── actions.ts      # Server Actions (camada fina)
│   │   ├── service.ts      # regras de negócio (funções puras quando possível)
│   │   ├── repository.ts   # acesso a dados (Prisma isolado aqui)
│   │   └── schemas.ts      # Zod + tipos do domínio
│   ├── auth/
│   └── projetos/
├── components/ui/          # design system compartilhado (Button, Card...)
└── lib/                    # infra transversal: db, env, utils

A regra central: as dependências apontam para dentro. A página chama a action; a action valida e chama o service; o service decide e chama o repository; só o repository conhece o Prisma. Trocar o ORM (ou mockar o banco num teste) vira trabalho local, não cirurgia geral.

features/tarefas/service.tsTypeScript
import { tarefaRepo } from "./repository";
import { Result, Ok, Err } from "@/lib/result";

type ErroConcluir = "nao_encontrada" | "sem_permissao" | "ja_concluida";

export async function concluirTarefa(
  tarefaId: string,
  usuarioId: string
): Promise<Result<void, ErroConcluir>> {
  const tarefa = await tarefaRepo.buscarComDono(tarefaId);

  if (!tarefa) return Err("nao_encontrada");
  if (tarefa.donoId !== usuarioId) return Err("sem_permissao");
  if (tarefa.status === "CONCLUIDA") return Err("ja_concluida");

  await tarefaRepo.atualizarStatus(tarefaId, "CONCLUIDA");
  return Ok(undefined);
}
// Regra de negócio testável SEM Next.js, SEM HTTP, SEM mock de framework.

12.2 Padrões que valem citar em entrevista

  • Repository: isola o acesso a dados atrás de uma interface (você viu acima).
  • Factory: centraliza criação de objetos complexos (ex.: montar um cliente de pagamento por gateway).
  • Strategy: comportamentos intercambiáveis — Record<Metodo, (p: Pedido) => Promise<Result>> em vez de if/else gigante.
  • Adapter: embrulha SDKs externos (Stripe, S3) num contrato seu; trocar fornecedor deixa de ser reescrita.
  • Injeção de dependência simples: passe dependências como parâmetro (criarService({ repo, clock })) — não precisa de framework de DI para ter testabilidade.

12.3 Princípios acima de padrões

  • SRP (responsabilidade única): um módulo, um motivo para mudar.
  • Composição > herança: em TS/React, praticamente sempre.
  • Parse, don't validate: valide na fronteira (Zod) e circule tipos ricos por dentro — o interior do sistema nunca vê unknown.
  • Torne estados inválidos irrepresentáveis: { status: 'sucesso'; dados: T } | { status: 'erro'; msg: string } em vez de { dados?: T; erro?: string; carregando?: boolean } onde combinações impossíveis compilam.
  • YAGNI: não crie abstração para necessidade hipotética. Sênior também é saber não abstrair.

12.4 Monorepos (contexto de empresas maiores)

Times grandes organizam múltiplos apps e pacotes num único repositório com pnpm workspaces + Turborepo: apps/web, apps/api, packages/ui, packages/config. Benefícios: compartilhar tipos e componentes sem publicar em registro, builds com cache incremental. Saber o conceito e o vocabulário já ajuda em entrevistas para empresas maiores.

Exercícios do módulo 12
  1. Refatore a feature de tarefas do DevBoard para a estrutura action → service → repository, com o service retornando Result.
  2. Escreva testes unitários do service passando um repository fake por parâmetro (sem tocar no banco).
  3. Implemente notificações (e-mail/console) com o padrão Strategy, escolhendo a estratégia por configuração.
  4. Encontre no seu código um tipo com campos opcionais que permitem estados impossíveis e refatore para discriminated union.
M13

Performance e otimização

Nível: avançado · Tempo sugerido: 1–2 semanas

Performance é dinheiro: conversão, SEO e custo de infraestrutura. Next.js entrega muita otimização de graça — seu trabalho é não desligá-la e medir o que importa.

13.1 Core Web Vitals: o vocabulário

MétricaMedeMetaVilões comuns
LCPTempo até o maior elemento aparecer< 2,5sImagens pesadas, servidor lento
INPResposta às interações< 200msJavaScript demais no cliente
CLSEstabilidade visual (pulos de layout)< 0,1Imagens/anúncios sem dimensão

Ferramentas: Lighthouse (DevTools), PageSpeed Insights e o profiler do React DevTools. Meça antes de otimizar — otimização sem medição é chute.

13.2 As otimizações nativas do Next.js

otimizacoes.tsxTSX
// 1. Imagens: redimensiona, converte p/ WebP/AVIF, lazy load, evita CLS
import Image from "next/image";
<Image src="/capa.jpg" alt="Capa" width={1200} height={630} priority />
// priority: use na imagem do LCP (acima da dobra)

// 2. Fontes: self-host automático, sem flash de fonte errada
import { Inter } from "next/font/google";
const inter = Inter({ subsets: ["latin"] });

// 3. Code splitting: cada rota já é um bundle separado.
//    Componentes pesados só quando necessários:
import dynamic from "next/dynamic";
const Grafico = dynamic(() => import("./grafico"), {
  loading: () => <Skeleton />,
});

13.3 Estratégia de performance em RSC

  • Menos JavaScript no cliente: a otimização nº 1 do App Router é manter componentes como Server Components. Antes de otimizar re-render, pergunte: "isso precisa ser client?"
  • Requisições em paralelo: Promise.all para dados independentes; await sequencial só quando um depende do outro.
  • Streaming (M06): Suspense em blocos lentos melhora LCP percebido.
  • Cache em camadas: ISR para conteúdo semi-estático; revalidateTag para invalidar cirurgicamente.
paralelo.tsTypeScript
// ❌ Cascata: 300ms + 300ms + 300ms = 900ms
const user = await buscarUser();
const projetos = await buscarProjetos();
const avisos = await buscarAvisos();

// ✅ Paralelo: ~300ms total
const [user2, projetos2, avisos2] = await Promise.all([
  buscarUser(), buscarProjetos(), buscarAvisos(),
]);

13.4 Otimizando o lado client

memo.tsxTSX
import { memo, useMemo, useCallback } from "react";

// useMemo: cachear CÁLCULO caro entre renders
const ordenados = useMemo(
  () => [...itens].sort((a, b) => b.pontos - a.pontos),
  [itens]
);

// useCallback + memo: evitar re-render de filho pesado
const aoSelecionar = useCallback((id: string) => setSel(id), []);
const LinhaMemo = memo(Linha);
⚠️ Não otimize no escuro

Memoizar tudo por padrão polui o código e raramente ajuda. Use o React DevTools Profiler para achar o re-render caro primeiro. E fique de olho no React Compiler, que automatiza boa parte dessa memoização — vale citar em entrevista que você acompanha isso.

  • Listas enormes: virtualização com TanStack Virtual (renderiza só o visível).
  • Inputs de busca: debounce (300ms) antes de disparar requisições.
  • Bundle: analise com @next/bundle-analyzer; troque libs pesadas (ex.: date-fns em vez de moment).
Exercícios do módulo 13
  1. Rode o Lighthouse no DevBoard, registre as notas, aplique 3 melhorias e compare (antes/depois no README — excelente para portfólio).
  2. Encontre uma cascata de await no seu código e paralelize com Promise.all.
  3. Adicione debounce à busca de projetos e virtualização a uma lista de 5.000 itens simulados.
  4. Rode o bundle analyzer e identifique a maior dependência do bundle client. Dá para removê-la ou carregá-la dinamicamente?
M14

Deploy, Docker e CI/CD

Nível: avançado · Tempo sugerido: 1–2 semanas

Projeto que não está no ar não existe para recrutador. Aqui você aprende o caminho fácil (Vercel), o caminho corporativo (Docker) e a esteira que roda testes a cada push.

14.1 Deploy na Vercel (o caminho padrão)

  1. Suba o projeto no GitHub.
  2. Em vercel.com, importe o repositório — o Next.js é detectado automaticamente.
  3. Configure as variáveis de ambiente (DATABASE_URL, AUTH_SECRET, chaves OAuth) no painel.
  4. Use um Postgres gerenciado (Neon/Supabase) com connection pooling habilitado.
  5. Cada Pull Request ganha um Preview Deploy com URL própria — mostre isso em entrevistas, é o fluxo real das empresas.
💡 Migrações no deploy

Adicione ao build: "build": "prisma generate && prisma migrate deploy && next build" — assim o banco de produção sempre acompanha o schema.

14.2 Docker: o padrão corporativo

Empresas com infraestrutura própria (AWS, GCP, Kubernetes) rodam Next.js em containers. Habilite output: "standalone" no next.config.ts e use um build multi-stage:

DockerfileDocker
# Etapa 1: dependências
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci

# Etapa 2: build
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npx prisma generate && npm run build

# Etapa 3: imagem final enxuta (~150MB)
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]

14.3 CI/CD com GitHub Actions

.github/workflows/ci.ymlYAML
name: CI
on:
  pull_request:
  push: { branches: [main] }

jobs:
  quality:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20, cache: npm }
      - run: npm ci
      - run: npm run lint          # ESLint
      - run: npx tsc --noEmit      # checagem de tipos
      - run: npm test -- --run     # Vitest
      - run: npm run build         # o build precisa passar

Com isso, todo PR só pode ser mesclado se lint, tipos, testes e build passarem — exatamente o portão de qualidade usado em empresas. Configure "branch protection" no GitHub exigindo o check verde.

14.4 Observabilidade mínima

  • Erros: Sentry (captura exceções de server e client com stack trace e release).
  • Logs estruturados: JSON com contexto (userId, rota) em vez de console.log solto.
  • Métricas de produto: Vercel Analytics / PostHog para Web Vitals reais e funil.
  • Uptime: um monitor gratuito (ex.: UptimeRobot) pingando sua rota /api/health.
Exercícios do módulo 14
  1. Publique o DevBoard na Vercel com banco Neon/Supabase e OAuth do GitHub funcionando em produção.
  2. Escreva o Dockerfile standalone e rode localmente com docker compose (app + Postgres).
  3. Configure o workflow de CI acima e quebre um teste de propósito para ver o PR bloqueado.
  4. Crie GET /api/health que checa a conexão com o banco e retorna { status: 'ok' }.
M15

Mercado de trabalho e portfólio

Nível: carreira · Tempo sugerido: contínuo

Habilidade técnica te qualifica; posicionamento te contrata. Este módulo transforma o que você construiu em entrevistas marcadas.

15.1 Portfólio que funciona: 2–3 projetos profundos

Recrutadores técnicos preferem poucos projetos completos a dezenas de clones de tutorial. Cada projeto do portfólio deve ter:

  • Deploy no ar com link funcionando (e usuário demo: demo@ex.com / demo1234).
  • README caprichado: screenshot/GIF, stack, decisões técnicas ("por que Server Actions e não REST aqui"), como rodar local.
  • Autenticação + banco de dados + testes + CI — o combo que prova "full stack".
  • Histórico de commits limpo com Conventional Commits.
  • Uma feature não-óbvia: upload de arquivos, pagamento (Stripe modo teste), tempo real, exportação de relatório — algo além do CRUD.
💡 Ideias de projeto além do DevBoard
  • Micro-SaaS de nicho: agendamento para barbearias, controle de comandas, gestor de assinaturas — problemas reais impressionam mais que clones.
  • Ferramenta dev: encurtador de URL com analytics, gerador de changelog a partir do GitHub.
  • Contribuição open source: uma issue good first issue aceita num projeto conhecido vale ouro no currículo.

15.2 O que as vagas pedem (e como se preparar)

NívelEsperam que você...Perguntas típicas
Estágio/JúniorDomine JS/TS básico, React, Git; aprenda rápido; comunique-se bemmap/filter, estado imutável, o que é uma API, diferença GET/POST
PlenoEntregue features de ponta a ponta sozinho, com testes e boas práticasSSR vs SSG, Server vs Client Components, N+1, JWT vs sessão, como estrutura um projeto
SêniorDecida arquitetura, faça trade-offs, mentore, pense em custo/escalaDesenho de sistema, estratégia de cache, migrações sem downtime, "conte uma decisão técnica difícil"
💼 Sobre salários e regime de trabalho

Faixas salariais variam muito por região, porte da empresa e câmbio — consulte pesquisas atuais (Glassdoor, levantamentos de comunidades como o da Rocketseat/Codigo Fonte TV, relatórios da GeekHunter) na época da sua busca. Vale saber: o mercado brasileiro tem muitas vagas remotas, e a stack TS/Next abre portas para vagas internacionais (contrato PJ via plataformas como Deel) — inglês intermediário para leitura/conversação técnica costuma ser o divisor de águas de remuneração.

15.3 Preparação para entrevistas técnicas

  1. Perguntas conceituais: revise os boxes "🎯" desta apostila; treine respostas de 2 minutos em voz alta.
  2. Live coding: pratique exercícios de arrays/objetos/strings em TypeScript falando o raciocínio enquanto codifica — avaliam comunicação tanto quanto código.
  3. Take-home: capriche em README, testes e commits; entregue algo pequeno e polido em vez de grande e quebrado.
  4. Sobre seu projeto: saiba defender cada decisão do DevBoard: "por que Prisma?", "como protege contra IDOR?", "o que mudaria com 100x mais usuários?".
  5. Comportamental: prepare 3 histórias no formato STAR (situação → tarefa → ação → resultado): um bug difícil, um conflito de opinião técnica, algo que você aprendeu errando.

15.4 Presença profissional

  • LinkedIn: título claro ("Desenvolvedor Full Stack | TypeScript, React, Next.js, Node"), seção "Projetos" com links, e 1 post por semana sobre o que estudou — recrutadores buscam por palavras-chave.
  • GitHub: README de perfil, pins nos melhores repositórios, contribuições consistentes.
  • Comunidades: He4rt, Frontend Fusion, servidores de Discord de tecnologia, meetups locais — a maioria das primeiras vagas vem de indicação.
  • Inglês técnico: 20 minutos por dia lendo documentação oficial em inglês já constrói o vocabulário necessário.

15.5 Roadmap de continuação

Depois desta apostila, os próximos degraus mais valorizados são: tRPC (RPC tipado ponta a ponta), TanStack Query a fundo, WebSockets/tempo real, filas e background jobs (BullMQ, Inngest), Stripe (billing de SaaS), React Native/Expo (reaproveitando seu TS) e fundamentos de AWS. Escolha um por vez, sempre construindo algo público.

🏁 Desafio final de portfólio

Evolua o DevBoard para um SaaS completo e público:

  • Times/workspaces com convites por e-mail (Resend) e papéis por workspace
  • Plano pago com Stripe em modo teste (checkout + webhook + bloqueio de features)
  • API pública documentada com chave de API e rate limiting
  • Suíte de testes no CI + preview deploys + Sentry em produção
  • Um post no LinkedIn/blog contando as decisões técnicas do projeto