🚀 Cap 06: Stack Enterprise TypeScript: NestJS (Clean Architecture) e Next.js App Router (SSR/RSC)
🗺️ 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):
PrismaServiceinjetável e global,CategoriaServiceeCategoriaController, 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
HttpOnlye omiddlewareque protege/admin. O módulo usa Next 14 (cookies()síncrono); no Next 15, acrescente oawait. - No código:
api-nest/prisma/schema.prismaé o modelo real (usuários, produtos, pedidos; usado no Cap. 07). Jáweb-nextjs/app/page.tsxtem os produtos escritos no próprio arquivo e umrevalidate = 60que regera a página sempre igual: é a última armadilha da Seção 6, e o exercício 06.3 a corrige.
- Módulo 02 (NestJS e Categoria):
fullstack_tecloja_03_nest_react: oapi-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.