🗺️ Mapa Arquitetural do Tópico

flowchart LR
    NAV["🌐 Navegador"] -->|"HTML pronto + ilhas interativas"| NEXT["▲ Next.js App Router<br/>Server Components · Client Components<br/>Route Handlers (BFF)"]
    NEXT -->|"fetch servidor → servidor<br/>cookie HttpOnly → Bearer"| NEST["🐈 NestJS<br/>Guard → Pipe → Controller → Service"]
    NEST --> PRISMA["🔷 Prisma"] --> PG[("🐘 PostgreSQL")]

    style NEXT fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
    style NEST fill:#fce4ec,stroke:#e91e63,stroke-width:2px
    style PG fill:#ede7f6,stroke:#673ab7,stroke-width:1px

🧩 1. O problema: liberdade demais nas duas pontas

No Cap. 05, cada decisão da API Express e do React era sua: onde ficam as pastas, como validar, como tratar erros, como buscar dados. Isso funciona para uma equipe pequena. Com dez pessoas no mesmo código, cada uma resolve de um jeito.

Do lado do navegador, um React que só roda no cliente (SPA) entrega uma página vazia e depois busca os dados: o usuário vê “Carregando…”, e o buscador do Google indexa uma página sem produtos.

  Express + React (Cap. 05) NestJS + Next.js (este capítulo)
Estrutura da API Livre Módulos, controllers e serviços impostos
Validação e erros Você monta (Zod, handler) Pipes, exceções HTTP e filtros prontos
Primeira página HTML vazio + JavaScript busca os dados HTML com os dados, gerado no servidor
SEO de uma vitrine Fraco Bom
Custo Pouca coisa para aprender Decorators, injeção de dependência, Server × Client Components

🐈 2. NestJS: módulos, controllers, serviços e injeção de dependência

O NestJS organiza a API em módulos. Cada módulo declara seus controllers (as rotas) e seus providers (os serviços), e o Nest injeta os serviços nos construtores:

@Injectable()                         // grava os tipos do construtor para o Nest saber o que injetar
export class ProdutosService {
  private produtos: Produto[] = [];
  private proximoId = 1;

  buscar(id: number): Produto {
    const p = this.produtos.find((x) => x.id === id);
    if (!p) throw new NotFoundException(`produto ${id} não encontrado`);   // vira 404 sozinho
    return p;
  }

  criar(dto: CriarProdutoDto): Produto {
    const p = { id: this.proximoId++, ...dto };
    this.produtos.push(p);
    return p;
  }
}

@Controller("produtos")
export class ProdutosController {
  constructor(private readonly servico: ProdutosService) {}   // injeção pelo construtor

  @Get(":id")
  buscar(@Param("id", ParseIntPipe) id: number) { return this.servico.buscar(id); }

  @Post()
  @UseGuards(PapeisGuard)
  @Papeis("ADMIN")
  criar(@Body() dto: CriarProdutoDto) { return this.servico.criar(dto); }
}

@Module({ controllers: [ProdutosController], providers: [ProdutosService, PapeisGuard] })
export class ProdutosModule {}

Por que injeção de dependência? O controller não faz new ProdutosService(). Ele declara o que precisa, e o container do Nest entrega. No teste, o mesmo controller recebe um serviço falso sem mudar uma linha. É a Clean Architecture do Cap. 03 com o framework fazendo a ligação.

🛂 3. O caminho de uma requisição: guards, pipes e exceções

Antes de chegar ao método do controller, a requisição passa por uma sequência fixa. Cada etapa pode parar a requisição com o status certo:

Etapa Pergunta Exemplo Se falhar
Guard Pode entrar? PapeisGuard exige ADMIN 403 Forbidden
Pipe Os dados estão certos? ValidationPipe, ParseIntPipe 400 Bad Request
Controller → Service Executa servico.criar(dto) Exceção HTTP lançada (ex.: 404)
Filtro de exceção Como responder o erro? Padrão do Nest JSON { statusCode, message, error }

A validação fica declarada no DTO com class-validator e é ligada uma vez, globalmente:

export class CriarProdutoDto {
  @IsString() @MinLength(3) nome!: string;
  @IsPositive() preco!: number;
  @IsInt() @IsPositive() estoque!: number;
}

// main.ts — whitelist descarta campos fora do DTO; forbidNonWhitelisted responde 400 se vierem.
app.useGlobalPipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true }));

Rodando esse app, um POST /produtos com {"nome":"TV","preco":-1,"estoque":2.5,"desconto":90} responde 400 com as quatro mensagens: property desconto should not exist, nome must be longer than or equal to 3 characters, preco must be a positive number e estoque must be an integer number. GET /produtos/abc responde 400 pelo ParseIntPipe, e sem o papel ADMIN o POST responde 403 pelo guard. O controller não tem nenhum if de validação.

O whitelist é uma defesa de segurança, não um detalhe: sem ele, um campo extra que o cliente mande ("role": "ADMIN", "preco": 0.01) passa direto para o serviço. É o ataque de mass assignment.

▲ 4. Next.js App Router: Server Components, Client Components e BFF

No App Router, todo componente é Server Component por padrão: roda no servidor, pode ser async e chama a API direto, sem expor nada ao navegador:

// app/page.tsx — Server Component
export default async function CatalogoPage() {
  const resp = await fetch(`${process.env.API_URL}/produtos`, {
    next: { revalidate: 60 },          // ISR: reaproveita o HTML por 60 s e regera em segundo plano
  });
  if (!resp.ok) throw new Error(`API respondeu ${resp.status}`);   // cai no error.tsx da rota
  const produtos: Produto[] = await resp.json();

  return (
    <main>
      {produtos.map((p) => (
        <article key={p.id}>
          <h2>{p.nome}</h2>
          <BotaoComprar produtoId={p.id} disponivel={p.estoque > 0} />
        </article>
      ))}
    </main>
  );
}

O que precisa de estado ou eventos (clique, useState) vira um Client Component, marcado com "use client". Ele é uma “ilha” interativa dentro da página gerada no servidor:

"use client";   // Client Component: tem estado e eventos, roda no navegador
import { useState } from "react";

export function BotaoComprar({ produtoId, disponivel }: { produtoId: number; disponivel: boolean }) {
  const [adicionado, setAdicionado] = useState(false);
  return (
    <button disabled={!disponivel || adicionado} onClick={() => setAdicionado(true)}>
      {adicionado ? "No carrinho" : disponivel ? `Comprar #${produtoId}` : "Esgotado"}
    </button>
  );
}

4.1 O Next.js como BFF: o token nunca chega ao navegador

Um Route Handler (app/api/.../route.ts) é uma rota de backend dentro do Next. Usado como BFF (Backend for Frontend), ele recebe o login, chama o NestJS de servidor para servidor e guarda o JWT num cookie HttpOnly, que o JavaScript da página não consegue ler (a recomendação do Cap. 02):

// app/api/auth/login/route.ts
export async function POST(request: Request) {
  const resp = await fetch(`${process.env.API_URL}/auth/login`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(await request.json()),
  });
  if (!resp.ok) return NextResponse.json({ erro: "credenciais inválidas" }, { status: 401 });

  const { token, usuario } = await resp.json();
  (await cookies()).set("tecloja_token", token, {    // Next 15: cookies() é assíncrono
    httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: "lax", path: "/", maxAge: 60 * 60,
  });
  return NextResponse.json({ usuario });              // o navegador nunca vê o JWT
}

Next 14 × Next 15. Duas mudanças pegam quem segue tutoriais antigos: no 15, cookies() e headers() são assíncronos (await cookies()), e o fetch deixou de ir para o cache por padrão. No 14, um fetch sem opções era guardado para sempre; no 15, é refeito a cada requisição. Por isso o código acima diz explicitamente o que quer: revalidate: 60.

⚖️ 5. Comparação: como cada página é gerada

Modo Como pedir no App Router Quando o HTML é gerado Use para
SSG (estático) fetch(url, { cache: "force-cache" }) No build Páginas institucionais, documentação
ISR fetch(url, { next: { revalidate: 60 } }) No build e regerado a cada N segundos Vitrine, catálogo, blog
SSR (dinâmico) fetch(url, { cache: "no-store" }) ou uso de cookies() A cada requisição Carrinho, área logada, estoque em tempo real
CSR (no cliente) Client Component com TanStack Query No navegador, depois de carregar Painéis muito interativos, dados por usuário

⚠️ 6. Armadilhas comuns

Sintoma Causa Correção
Nest can't resolve dependencies of the ProdutosController (?) na subida Serviço fora dos providers do módulo (ou de outro módulo sem exports) Registrar no módulo, e exportar se outro módulo usa
App sobe, mas a 1ª chamada dá Cannot read properties of undefined Serviço sem @Injectable() que tem dependências no construtor: sem o decorator, o TypeScript não grava os tipos dos parâmetros e o Nest injeta undefined @Injectable() em todo provider, mesmo os que hoje não têm dependências
Campo extra (role, preco) gravado no banco ValidationPipe sem whitelist whitelist: true (e forbidNonWhitelisted: true)
@IsInt() falha para "5" vindo da URL Parâmetros de rota chegam como texto ParseIntPipe no parâmetro ou transform: true
A página inteira vira JavaScript no navegador "use client" no layout.tsx ou num componente alto da árvore "use client" só nas folhas interativas
Chave secreta aparece no código-fonte da página process.env.SEGREDO usado num Client Component Segredos só em Server Components e Route Handlers
next build falha com “fetch failed” O build tenta gerar páginas estáticas chamando a API, que não está no ar API disponível no build, ou a rota como dinâmica (no-store)
Erro de hydration O HTML do servidor e o primeiro render do cliente diferem (Date.now(), Math.random(), localStorage no render) Valores variáveis em useEffect ou só no servidor
revalidate = 60 e a vitrine nunca muda Os dados estão no próprio código, não vêm de fetch Buscar da API; o revalidate regera a página, mas só muda o que vem de fora

🗂️ 7. No Catálogo de Projetos

Os dois projetos são do tipo guia (📘): o passo a passo completo está nos módulos, e o código no repositório é parcial. Vale estudar os módulos e, ao mesmo tempo, olhar criticamente o código:

  • fullstack_tecloja_04_nest_nextjs, a referência deste capítulo:
    • Módulo 02 (NestJS e Categoria): PrismaService injetável e global, CategoriaService e CategoriaController, a Seção 2 deste tópico aplicada;
    • Módulo 03 (DTOs e Validação) e Módulo 05 (JWT e Guards): a Seção 3;
    • Módulo 06 (Next.js e RSC): o catálogo como Server Component chamando o NestJS com cache: 'no-store';
    • Módulo 07 (BFF e Route Handlers): o login que guarda o JWT em cookie HttpOnly e o middleware que protege /admin. O módulo usa Next 14 (cookies() síncrono); no Next 15, acrescente o await.
    • No código: api-nest/prisma/schema.prisma é o modelo real (usuários, produtos, pedidos; usado no Cap. 07). Já web-nextjs/app/page.tsx tem os produtos escritos no próprio arquivo e um revalidate = 60 que regera a página sempre igual: é a última armadilha da Seção 6, e o exercício 06.3 a corrige.
  • fullstack_tecloja_03_nest_react: o api-nest/src/products/products.service.ts é uma classe sem @Injectable(), sem controller e sem módulo: sozinha, não forma um app Nest. Os módulos 03 a 05 do projeto mostram como deveria ficar. É o ponto de partida do exercício 06.1.