<h2>Introdução ao Next.js com TypeScript e App Router</h2>
<p>O Next.js evoluiu significativamente na versão 13+ com a introdução do App Router, um paradigma completamente novo para estruturar aplicações React modernas. Se você já trabalhou com o Pages Router (estrutura anterior), perceberá que estamos falando de uma mudança fundamental na forma como organizamos rotas, componentes e lógica de servidor. O App Router traz consigo a capacidade nativa de trabalhar com Server Components, permitindo renderizar componentes no servidor e enviar apenas HTML para o cliente, reduzindo JavaScript no navegador.</p>
<p>TypeScript complementa perfeitamente essa arquitetura, fornecendo segurança de tipos em tempo de desenvolvimento e evitando erros comuns em produção. Neste artigo, você compreenderá como estruturar um projeto Next.js moderno, aproveitando plenamente o App Router, Server Components e tipagem TypeScript rigorosa.</p>
<h2>Estrutura do App Router e Sistema de Arquivo</h2>
<h3>Como funciona o roteamento baseado em arquivo</h3>
<p>O App Router do Next.js utiliza uma convenção de diretórios dentro da pasta <code>app/</code>. Diferentemente do Pages Router onde <code>pages/usuarios.tsx</code> gerava a rota <code>/usuarios</code>, aqui criamos uma pasta <code>app/usuarios/</code> contendo um arquivo <code>page.tsx</code>. Essa abordagem oferece melhor organização de código relacionado (layout, componentes internos, estilos) no mesmo diretório.</p>
<p>A estrutura básica é intuitiva: qualquer pasta dentro de <code>app/</code> com um arquivo <code>page.tsx</code> ou <code>page.js</code> se torna uma rota. Pastas que começam com colchetes <code>[param]</code> são segmentos dinâmicos. Você pode também criar layouts compartilhados com <code>layout.tsx</code>, componentes com <code>loading.tsx</code> para estados de carregamento, <code>error.tsx</code> para tratamento de erros, e <code>not-found.tsx</code> para páginas não encontradas.</p>
<pre><code class="language-typescript">// app/layout.tsx - Layout raiz que envolve toda aplicação
import type { Metadata } from 'next';
import './globals.css';
export const metadata: Metadata = {
title: 'Minha Aplicação',
description: 'Aplicação Next.js com TypeScript',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="pt-BR">
<body>{children}</body>
</html>
);
}</code></pre>
<pre><code class="language-typescript">// app/page.tsx - Página inicial (rota /)
export default function Home() {
return <h1>Bem-vindo ao Next.js 13+</h1>;
}</code></pre>
<pre><code class="language-typescript">// app/usuarios/page.tsx - Página da rota /usuarios
export default function UsuariosPage() {
return <h1>Lista de Usuários</h1>;
}</code></pre>
<pre><code class="language-typescript">// app/usuarios/[id]/page.tsx - Página dinâmica /usuarios/:id
interface UsuarioPageProps {
params: {
id: string;
};
}
export default function UsuarioPage({ params }: UsuarioPageProps) {
return <h1>Usuário ID: {params.id}</h1>;
}</code></pre>
<h3>Segmentos especiais e suas funções</h3>
<p>Além de <code>page.tsx</code>, o App Router reconhece outros arquivos especiais que modificam o comportamento da rota. O <code>layout.tsx</code> define uma UI que persiste entre navegações no mesmo segmento, ideal para sidebars e navegações. O <code>loading.tsx</code> exibe um esqueleto ou spinner enquanto o conteúdo carrega (funciona com Suspense). O <code>error.tsx</code> captura erros do segmento e do seu subtree, exibindo um UI alternativa. Por fim, <code>not-found.tsx</code> é renderizado quando você chama <code>notFound()</code> ou para rotas inexistentes.</p>
<pre><code class="language-typescript">// app/usuarios/layout.tsx - Layout específico para usuários
import type { ReactNode } from 'react';
export default function UsuariosLayout({ children }: { children: ReactNode }) {
return (
<div className="usuarios-container">
<aside className="usuarios-sidebar">
<nav>
<a href="/usuarios">Ver Todos</a>
</nav>
</aside>
<main>{children}</main>
</div>
);
}</code></pre>
<pre><code class="language-typescript">// app/usuarios/loading.tsx - Mostra durante o carregamento
export default function UsuariosLoading() {
return <div className="spinner">Carregando usuários...</div>;
}</code></pre>
<pre><code class="language-typescript">// app/usuarios/error.tsx - Tratador de erros
'use client';
import type { ReactNode } from 'react';
interface ErrorProps {
error: Error & { digest?: string };
reset: () => void;
}
export default function UsuariosError({ error, reset }: ErrorProps) {
return (
<div>
<h2>Erro ao carregar usuários</h2>
<p>{error.message}</p>
<button onClick={() => reset()}>Tentar novamente</button>
</div>
);
}</code></pre>
<h2>Server Components vs Client Components</h2>
<h3>O paradigma de Server Components</h3>
<p>Server Components são a principal inovação do App Router. Eles são renderizados exclusivamente no servidor, nunca no navegador. Isso significa que você pode acessar bancos de dados, APIs internas, variáveis de ambiente secretas e bibliotecas pesadas sem expô-las ao cliente. O servidor envia apenas HTML para o navegador, reduzindo drasticamente o bundle de JavaScript.</p>
<p>Por padrão, todos os componentes no App Router são Server Components. Você não precisa fazer nada especial — apenas crie um arquivo <code>.tsx</code> e exporte um componente. A magia acontece naturalmente. Isso é oposto ao comportamento do Pages Router, onde tudo era Client Component.</p>
<pre><code class="language-typescript">// app/usuarios/usuarios-lista.tsx - Server Component por padrão
import type { Usuario } from '@/types/usuario';
// Pode ser async!
async function buscarUsuarios(): Promise<Usuario[]> {
const response = await fetch('https://api.exemplo.com/usuarios', {
// Revalidar a cada 60 segundos
next: { revalidate: 60 },
});
if (!response.ok) throw new Error('Falha ao buscar usuários');
return response.json();
}
export default async function UsuariosLista() {
const usuarios = await buscarUsuarios();
return (
<ul>
{usuarios.map((usuario) => (
<li key={usuario.id}>{usuario.nome}</li>
))}
</ul>
);
}</code></pre>
<h3>Quando usar Client Components</h3>
<p>Client Components são necessários para interatividade: estado com <code>useState</code>, efeitos com <code>useEffect</code>, context providers, listeners de eventos e hooks customizados. Para marcar um arquivo como Client Component, adicione <code>'use client'</code> no topo. Componentes Client podem ser filhos de Server Components (mas não o inverso em arquivos separados).</p>
<p>Uma prática essencial é manter Client Components pequenos e focados. Coloque a lógica interativa em um Client Component minúsculo e envolva-o com um Server Component que faz o trabalho pesado. Isso maximiza os benefícios de performance.</p>
<pre><code class="language-typescript">// app/usuarios/filtro-usuarios.tsx - Client Component
'use client';
import { useState } from 'react';
interface FiltroUsuariosProps {
onFiltrar: (termo: string) => void;
}
export default function FiltroUsuarios({ onFiltrar }: FiltroUsuariosProps) {
const [termo, setTermo] = useState('');
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const novoTermo = e.target.value;
setTermo(novoTermo);
onFiltrar(novoTermo);
};
return (
<input
type="text"
placeholder="Filtrar usuários..."
value={termo}
onChange={handleChange}
className="filtro-input"
/>
);
}</code></pre>
<pre><code class="language-typescript">// app/usuarios/page.tsx - Server Component que incorpora Client Component
import FiltroUsuarios from './filtro-usuarios';
async function buscarUsuarios(filtro?: string) {
const query = filtro ? ?q=${filtro} : '';
const response = await fetch(https://api.exemplo.com/usuarios${query}, {
next: { revalidate: 60 },
});
return response.json();
}
export default async function UsuariosPage() {
const usuarios = await buscarUsuarios();
// Função para passar ao Client Component
const handleFiltrar = async (termo: string) => {
// Esta função executará no cliente, mas pode chamar uma Server Action
'use server';
return buscarUsuarios(termo);
};
return (
<div>
<FiltroUsuarios onFiltrar={handleFiltrar} />
<ul>
{usuarios.map((u) => (
<li key={u.id}>{u.nome}</li>
))}
</ul>
</div>
);
}</code></pre>
<h2>Tipos de Rota e Casos de Uso Específicos</h2>
<h3>Rotas estáticas e dinâmicas</h3>
<p>Rotas estáticas são pré-renderizadas em build time e servidas de um cache. Se você tem uma página de blog com 1000 posts, o Next.js pode gerar HTML para cada post durante o build. Para rotas dinâmicas com parâmetros, use <code>generateStaticParams</code> para indicar quais parâmetros devem ser pré-renderizados. Qualquer rota não coberta será renderizada sob demanda (ISR - Incremental Static Regeneration).</p>
<pre><code class="language-typescript">// app/blog/[slug]/page.tsx
interface BlogPostPageProps {
params: {
slug: string;
};
}
// Gera estas rotas em build time
export async function generateStaticParams() {
const posts = await fetch('https://api.exemplo.com/posts').then(r => r.json());
return posts.map((post) => ({
slug: post.slug,
}));
}
export default async function BlogPostPage({ params }: BlogPostPageProps) {
const post = await fetch(https://api.exemplo.com/posts/${params.slug}).then(r => r.json());
return (
<article>
<h1>{post.titulo}</h1>
<p>{post.conteudo}</p>
</article>
);
}</code></pre>
<h3>Rotas de API (API Routes)</h3>
<p>Rotas de API permitem criar endpoints HTTP sem servidor separado. Coloque arquivos <code>route.ts</code> em pastas dentro de <code>app/api/</code>. Eles funcionam como handlers HTTP puros, recebendo <code>NextRequest</code> e retornando <code>NextResponse</code>.</p>
<pre><code class="language-typescript">// app/api/usuarios/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const id = searchParams.get('id');
// Buscar usuários de um banco de dados
const usuarios = await fetch('https://seu-banco.com/usuarios').then(r => r.json());
return NextResponse.json({ usuarios }, { status: 200 });
}
export async function POST(request: NextRequest) {
const body = await request.json();
// Validar dados com TypeScript
if (!body.nome || !body.email) {
return NextResponse.json(
{ erro: 'Nome e email são obrigatórios' },
{ status: 400 }
);
}
// Salvar no banco
const novoUsuario = await fetch('https://seu-banco.com/usuarios', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
}).then(r => r.json());
return NextResponse.json(novoUsuario, { status: 201 });
}</code></pre>
<pre><code class="language-typescript">// app/api/usuarios/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server';
interface RouteParams {
params: {
id: string;
};
}
export async function GET(request: NextRequest, { params }: RouteParams) {
const usuario = await fetch(https://seu-banco.com/usuarios/${params.id}).then(r => r.json());
if (!usuario) {
return NextResponse.json({ erro: 'Usuário não encontrado' }, { status: 404 });
}
return NextResponse.json(usuario);
}
export async function PATCH(request: NextRequest, { params }: RouteParams) {
const body = await request.json();
const usuarioAtualizado = await fetch(https://seu-banco.com/usuarios/${params.id}, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
}).then(r => r.json());
return NextResponse.json(usuarioAtualizado);
}</code></pre>
<h3>Server Actions</h3>
<p>Server Actions são funções assíncronas que executam no servidor, sendo chamadas diretamente do cliente. Marque uma função com <code>'use server'</code> e ela pode ser invocada em Client Components. Isso elimina a necessidade de criar endpoints de API para operações simples.</p>
<pre><code class="language-typescript">// app/usuarios/acoes.ts - Arquivo com Server Actions
'use server';
import type { Usuario } from '@/types/usuario';
export async function criarUsuario(dados: Omit<Usuario, 'id'>) {
const response = await fetch('https://seu-banco.com/usuarios', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(dados),
});
if (!response.ok) {
throw new Error('Falha ao criar usuário');
}
return response.json();
}
export async function deletarUsuario(id: string) {
const response = await fetch(https://seu-banco.com/usuarios/${id}, {
method: 'DELETE',
});
if (!response.ok) {
throw new Error('Falha ao deletar usuário');
}
return true;
}</code></pre>
<pre><code class="language-typescript">// app/usuarios/form-novo-usuario.tsx - Client Component usando Server Action
'use client';
import { useState } from 'react';
import { criarUsuario } from './acoes';
export default function FormNovoUsuario() {
const [loading, setLoading] = useState(false);
const [erro, setErro] = useState<string | null>(null);
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setLoading(true);
setErro(null);
const formData = new FormData(e.currentTarget);
const dados = {
nome: formData.get('nome') as string,
email: formData.get('email') as string,
};
try {
await criarUsuario(dados);
alert('Usuário criado com sucesso!');
e.currentTarget.reset();
} catch (err) {
setErro(err instanceof Error ? err.message : 'Erro desconhecido');
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit}>
<input name="nome" placeholder="Nome" required />
<input name="email" type="email" placeholder="Email" required />
<button type="submit" disabled={loading}>
{loading ? 'Criando...' : 'Criar Usuário'}
</button>
{erro && <p className="erro">{erro}</p>}
</form>
);
}</code></pre>
<h2>Tipagem TypeScript Avançada no Next.js</h2>
<h3>Tipando Props de Componentes e Páginas</h3>
<p>No App Router, as props que as páginas recebem seguem padrões específicos. Páginas recebem <code>params</code> (segmentos dinâmicos) e <code>searchParams</code> (query string). Componentes podem receber <code>children</code> e outras props customizadas. Tipar isso corretamente evita erros e facilita manutenção.</p>
<pre><code class="language-typescript">// app/produtos/[categoria]/[id]/page.tsx - Página com múltiplos parâmetros
import type { ReactNode } from 'react';
interface ProdutoPageProps {
params: {
categoria: string;
id: string;
};
searchParams: {
highlight?: string;
origem?: string;
};
}
export default async function ProdutoPage({
params,
searchParams,
}: ProdutoPageProps) {
const { categoria, id } = params;
const { highlight, origem } = searchParams;
const produto = await fetch(
https://api.exemplo.com/produtos/${categoria}/${id}
).then(r => r.json());
return (
<div>
<h1>{produto.nome}</h1>
{highlight && <p className="destaque">{highlight}</p>}
<p>Origem: {origem || 'Desconhecida'}</p>
</div>
);
}</code></pre>
<h3>Criando tipos customizados reutilizáveis</h3>
<p>Defina interfaces e tipos em um arquivo <code>types/</code> centralizado. Isso melhora a organização e permite reutilização em toda a aplicação. Use <code>type</code> para tipos simples e <code>interface</code> para objetos complexos que podem ser estendidos.</p>
<pre><code class="language-typescript">// types/usuario.ts
export interface Usuario {
id: string;
nome: string;
email: string;
criado_em: Date;
ativo: boolean;
}
export type NovoUsuario = Omit<Usuario, 'id' | 'criado_em'>;
export interface RespuestaAPI<T> {
dados: T;
erro: string | null;
status: number;
}
export interface UsuarioComPosts extends Usuario {
posts: Post[];
}
// types/post.ts
export interface Post {
id: string;
titulo: string;
conteudo: string;
autor_id: string;
criado_em: Date;
}</code></pre>
<pre><code class="language-typescript">// app/usuarios/[id]/page.tsx - Usando tipos customizados
import type { Usuario, UsuarioComPosts } from '@/types/usuario';
interface PageProps {
params: {
id: string;
};
}
async function buscarUsuarioComPosts(id: string): Promise<UsuarioComPosts> {
const response = await fetch(
https://api.exemplo.com/usuarios/${id}?include=posts
);
if (!response.ok) throw new Error('Usuário não encontrado');
return response.json();
}
export default async function UsuarioPage({ params }: PageProps) {
const usuario = await buscarUsuarioComPosts(params.id);
return (
<div>
<h1>{usuario.nome}</h1>
<p>{usuario.email}</p>
<section>
<h2>Posts ({usuario.posts.length})</h2>
<ul>
{usuario.posts.map((post) => (
<li key={post.id}>{post.titulo}</li>
))}
</ul>
</section>
</div>
);
}</code></pre>
<h3>Tipando variáveis de ambiente</h3>
<p>Use um arquivo <code>env.ts</code> para tipar suas variáveis de ambiente e garantir que todas as variáveis necessárias existam.</p>
<pre><code class="language-typescript">// env.ts
const requiredEnvVars = [
'NEXT_PUBLIC_API_URL',
'DATABASE_URL',
'JWT_SECRET',
] as const;
type EnvVar = typeof requiredEnvVars[number];
function getEnv(key: EnvVar): string {
const value = process.env[key];
if (!value) {
throw new Error(Variável de ambiente ${key} não definida);
}
return value;
}
export const env = {
apiUrl: process.env.NEXT_PUBLIC_API_URL!,
databaseUrl: process.env.DATABASE_URL!,
jwtSecret: process.env.JWT_SECRET!,
} as const;
// Usar na aplicação
// import { env } from '@/env';
// const resposta = await fetch(${env.apiUrl}/usuarios);</code></pre>
<h2>Conclusão</h2>
<p>Ao dominar o Next.js 13+ com TypeScript e App Router, você aprendeu três conceitos fundamentais que transformam a forma como você constrói aplicações web. Primeiro, o sistema de arquivos intuitivo do App Router elimina a necessidade de rotas explícitas — a estrutura de pastas é sua configuração. Segundo, Server Components por padrão reduzem drasticamente o JavaScript enviado ao cliente, melhorando performance e segurança ao permitir acesso seguro a dados sensíveis. Terceiro, TypeScript rigoroso nas props de componentes, tipos customizados e variáveis de ambiente previne classes inteiras de bugs em produção. Esses três pilares — roteamento inteligente, renderização no servidor e tipagem forte — formam a base de aplicações Next.js modernas, escaláveis e mantíveis.</p>
<h2>Referências</h2>
<ul>
<li><a href="https://nextjs.org/docs/app" target="_blank" rel="noopener noreferrer">Documentação Oficial do Next.js - App Router</a></li>
<li><a href="https://react.dev/reference/rsc/server-components" target="_blank" rel="noopener noreferrer">React Server Components - Documentação React</a></li>
<li><a href="https://nextjs.org/docs/app/building-your-application/configuring/typescript" target="_blank" rel="noopener noreferrer">TypeScript no Next.js - Guia Oficial</a></li>
<li><a href="https://nextjs.org/docs/app/building-your-application/routing/route-handlers" target="_blank" rel="noopener noreferrer">Next.js API Routes e Server Actions</a></li>
<li><a href="https://vercel.com/docs/concepts/nextjs/overview" target="_blank" rel="noopener noreferrer">Vercel - Best Practices for Next.js Performance</a></li>
</ul>
<p><!-- FIM --></p>