TypeScript

Next.js com TypeScript: App Router, Server Components e Tipos de Rota na Prática

19 min de leitura

Next.js com TypeScript: App Router, Server Components e Tipos de Rota na Prática

Introdução ao Next.js com TypeScript e App Router 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. 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. Estrutura do App Router e Sistema de Arquivo Como funciona o roteamento baseado em arquivo O App Router do Next.js utiliza uma convenção de diretórios dentro da pasta . Diferentemente do Pages Router

<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 &#039;next&#039;;

import &#039;./globals.css&#039;;

export const metadata: Metadata = {

title: &#039;Minha Aplicação&#039;,

description: &#039;Aplicação Next.js com TypeScript&#039;,

};

export default function RootLayout({

children,

}: {

children: React.ReactNode;

}) {

return (

&lt;html lang=&quot;pt-BR&quot;&gt;

&lt;body&gt;{children}&lt;/body&gt;

&lt;/html&gt;

);

}</code></pre>

<pre><code class="language-typescript">// app/page.tsx - Página inicial (rota /)

export default function Home() {

return &lt;h1&gt;Bem-vindo ao Next.js 13+&lt;/h1&gt;;

}</code></pre>

<pre><code class="language-typescript">// app/usuarios/page.tsx - Página da rota /usuarios

export default function UsuariosPage() {

return &lt;h1&gt;Lista de Usuários&lt;/h1&gt;;

}</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 &lt;h1&gt;Usuário ID: {params.id}&lt;/h1&gt;;

}</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 &#039;react&#039;;

export default function UsuariosLayout({ children }: { children: ReactNode }) {

return (

&lt;div className=&quot;usuarios-container&quot;&gt;

&lt;aside className=&quot;usuarios-sidebar&quot;&gt;

&lt;nav&gt;

&lt;a href=&quot;/usuarios&quot;&gt;Ver Todos&lt;/a&gt;

&lt;/nav&gt;

&lt;/aside&gt;

&lt;main&gt;{children}&lt;/main&gt;

&lt;/div&gt;

);

}</code></pre>

<pre><code class="language-typescript">// app/usuarios/loading.tsx - Mostra durante o carregamento

export default function UsuariosLoading() {

return &lt;div className=&quot;spinner&quot;&gt;Carregando usuários...&lt;/div&gt;;

}</code></pre>

<pre><code class="language-typescript">// app/usuarios/error.tsx - Tratador de erros

&#039;use client&#039;;

import type { ReactNode } from &#039;react&#039;;

interface ErrorProps {

error: Error &amp; { digest?: string };

reset: () =&gt; void;

}

export default function UsuariosError({ error, reset }: ErrorProps) {

return (

&lt;div&gt;

&lt;h2&gt;Erro ao carregar usuários&lt;/h2&gt;

&lt;p&gt;{error.message}&lt;/p&gt;

&lt;button onClick={() =&gt; reset()}&gt;Tentar novamente&lt;/button&gt;

&lt;/div&gt;

);

}</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 &#039;@/types/usuario&#039;;

// Pode ser async!

async function buscarUsuarios(): Promise&lt;Usuario[]&gt; {

const response = await fetch(&#039;https://api.exemplo.com/usuarios&#039;, {

// Revalidar a cada 60 segundos

next: { revalidate: 60 },

});

if (!response.ok) throw new Error(&#039;Falha ao buscar usuários&#039;);

return response.json();

}

export default async function UsuariosLista() {

const usuarios = await buscarUsuarios();

return (

&lt;ul&gt;

{usuarios.map((usuario) =&gt; (

&lt;li key={usuario.id}&gt;{usuario.nome}&lt;/li&gt;

))}

&lt;/ul&gt;

);

}</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>&#039;use client&#039;</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

&#039;use client&#039;;

import { useState } from &#039;react&#039;;

interface FiltroUsuariosProps {

onFiltrar: (termo: string) =&gt; void;

}

export default function FiltroUsuarios({ onFiltrar }: FiltroUsuariosProps) {

const [termo, setTermo] = useState(&#039;&#039;);

const handleChange = (e: React.ChangeEvent&lt;HTMLInputElement&gt;) =&gt; {

const novoTermo = e.target.value;

setTermo(novoTermo);

onFiltrar(novoTermo);

};

return (

&lt;input

type=&quot;text&quot;

placeholder=&quot;Filtrar usuários...&quot;

value={termo}

onChange={handleChange}

className=&quot;filtro-input&quot;

/&gt;

);

}</code></pre>

<pre><code class="language-typescript">// app/usuarios/page.tsx - Server Component que incorpora Client Component

import FiltroUsuarios from &#039;./filtro-usuarios&#039;;

async function buscarUsuarios(filtro?: string) {

const query = filtro ? ?q=${filtro} : &#039;&#039;;

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) =&gt; {

// Esta função executará no cliente, mas pode chamar uma Server Action

&#039;use server&#039;;

return buscarUsuarios(termo);

};

return (

&lt;div&gt;

&lt;FiltroUsuarios onFiltrar={handleFiltrar} /&gt;

&lt;ul&gt;

{usuarios.map((u) =&gt; (

&lt;li key={u.id}&gt;{u.nome}&lt;/li&gt;

))}

&lt;/ul&gt;

&lt;/div&gt;

);

}</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(&#039;https://api.exemplo.com/posts&#039;).then(r =&gt; r.json());

return posts.map((post) =&gt; ({

slug: post.slug,

}));

}

export default async function BlogPostPage({ params }: BlogPostPageProps) {

const post = await fetch(https://api.exemplo.com/posts/${params.slug}).then(r =&gt; r.json());

return (

&lt;article&gt;

&lt;h1&gt;{post.titulo}&lt;/h1&gt;

&lt;p&gt;{post.conteudo}&lt;/p&gt;

&lt;/article&gt;

);

}</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 &#039;next/server&#039;;

export async function GET(request: NextRequest) {

const { searchParams } = new URL(request.url);

const id = searchParams.get(&#039;id&#039;);

// Buscar usuários de um banco de dados

const usuarios = await fetch(&#039;https://seu-banco.com/usuarios&#039;).then(r =&gt; 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: &#039;Nome e email são obrigatórios&#039; },

{ status: 400 }

);

}

// Salvar no banco

const novoUsuario = await fetch(&#039;https://seu-banco.com/usuarios&#039;, {

method: &#039;POST&#039;,

headers: { &#039;Content-Type&#039;: &#039;application/json&#039; },

body: JSON.stringify(body),

}).then(r =&gt; 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 &#039;next/server&#039;;

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 =&gt; r.json());

if (!usuario) {

return NextResponse.json({ erro: &#039;Usuário não encontrado&#039; }, { 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: &#039;PATCH&#039;,

headers: { &#039;Content-Type&#039;: &#039;application/json&#039; },

body: JSON.stringify(body),

}).then(r =&gt; 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>&#039;use server&#039;</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

&#039;use server&#039;;

import type { Usuario } from &#039;@/types/usuario&#039;;

export async function criarUsuario(dados: Omit&lt;Usuario, &#039;id&#039;&gt;) {

const response = await fetch(&#039;https://seu-banco.com/usuarios&#039;, {

method: &#039;POST&#039;,

headers: { &#039;Content-Type&#039;: &#039;application/json&#039; },

body: JSON.stringify(dados),

});

if (!response.ok) {

throw new Error(&#039;Falha ao criar usuário&#039;);

}

return response.json();

}

export async function deletarUsuario(id: string) {

const response = await fetch(https://seu-banco.com/usuarios/${id}, {

method: &#039;DELETE&#039;,

});

if (!response.ok) {

throw new Error(&#039;Falha ao deletar usuário&#039;);

}

return true;

}</code></pre>

<pre><code class="language-typescript">// app/usuarios/form-novo-usuario.tsx - Client Component usando Server Action

&#039;use client&#039;;

import { useState } from &#039;react&#039;;

import { criarUsuario } from &#039;./acoes&#039;;

export default function FormNovoUsuario() {

const [loading, setLoading] = useState(false);

const [erro, setErro] = useState&lt;string | null&gt;(null);

const handleSubmit = async (e: React.FormEvent&lt;HTMLFormElement&gt;) =&gt; {

e.preventDefault();

setLoading(true);

setErro(null);

const formData = new FormData(e.currentTarget);

const dados = {

nome: formData.get(&#039;nome&#039;) as string,

email: formData.get(&#039;email&#039;) as string,

};

try {

await criarUsuario(dados);

alert(&#039;Usuário criado com sucesso!&#039;);

e.currentTarget.reset();

} catch (err) {

setErro(err instanceof Error ? err.message : &#039;Erro desconhecido&#039;);

} finally {

setLoading(false);

}

};

return (

&lt;form onSubmit={handleSubmit}&gt;

&lt;input name=&quot;nome&quot; placeholder=&quot;Nome&quot; required /&gt;

&lt;input name=&quot;email&quot; type=&quot;email&quot; placeholder=&quot;Email&quot; required /&gt;

&lt;button type=&quot;submit&quot; disabled={loading}&gt;

{loading ? &#039;Criando...&#039; : &#039;Criar Usuário&#039;}

&lt;/button&gt;

{erro &amp;&amp; &lt;p className=&quot;erro&quot;&gt;{erro}&lt;/p&gt;}

&lt;/form&gt;

);

}</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 &#039;react&#039;;

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 =&gt; r.json());

return (

&lt;div&gt;

&lt;h1&gt;{produto.nome}&lt;/h1&gt;

{highlight &amp;&amp; &lt;p className=&quot;destaque&quot;&gt;{highlight}&lt;/p&gt;}

&lt;p&gt;Origem: {origem || &#039;Desconhecida&#039;}&lt;/p&gt;

&lt;/div&gt;

);

}</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&lt;Usuario, &#039;id&#039; | &#039;criado_em&#039;&gt;;

export interface RespuestaAPI&lt;T&gt; {

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 &#039;@/types/usuario&#039;;

interface PageProps {

params: {

id: string;

};

}

async function buscarUsuarioComPosts(id: string): Promise&lt;UsuarioComPosts&gt; {

const response = await fetch(

https://api.exemplo.com/usuarios/${id}?include=posts

);

if (!response.ok) throw new Error(&#039;Usuário não encontrado&#039;);

return response.json();

}

export default async function UsuarioPage({ params }: PageProps) {

const usuario = await buscarUsuarioComPosts(params.id);

return (

&lt;div&gt;

&lt;h1&gt;{usuario.nome}&lt;/h1&gt;

&lt;p&gt;{usuario.email}&lt;/p&gt;

&lt;section&gt;

&lt;h2&gt;Posts ({usuario.posts.length})&lt;/h2&gt;

&lt;ul&gt;

{usuario.posts.map((post) =&gt; (

&lt;li key={post.id}&gt;{post.titulo}&lt;/li&gt;

))}

&lt;/ul&gt;

&lt;/section&gt;

&lt;/div&gt;

);

}</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 = [

&#039;NEXT_PUBLIC_API_URL&#039;,

&#039;DATABASE_URL&#039;,

&#039;JWT_SECRET&#039;,

] 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 &#039;@/env&#039;;

// 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>&lt;!-- FIM --&gt;</p>

Comentários

Mais em TypeScript

Dominando Mixins em TypeScript: Composição de Comportamentos sem Herança em Projetos Reais
Dominando Mixins em TypeScript: Composição de Comportamentos sem Herança em Projetos Reais

O Problema da Herança Clássica Quando começamos a programar orientada a objet...

Boas Práticas de Escrevendo Arquivos .d.ts: Tipando Bibliotecas JavaScript Existentes para Times Ágeis
Boas Práticas de Escrevendo Arquivos .d.ts: Tipando Bibliotecas JavaScript Existentes para Times Ágeis

O que são Arquivos .d.ts e Por Que Importam Os arquivos (TypeScript Declarati...

Union Types, Intersection Types e Type Guards em TypeScript: Do Básico ao Avançado
Union Types, Intersection Types e Type Guards em TypeScript: Do Básico ao Avançado

Union Types: Flexibilidade com Segurança Union Types permitem que uma variáve...