Tipar Fetch no Next.js com TypeScript
Domine data fetching tipado no Next.js. De fetch wrapper genérico até cache, revalidação e tratamento de erro, tudo com TypeScript garantindo segurança de tipos.
Por que isso é importante
Tipar Fetch no Next.js com TypeScript. Domine data fetching tipado no Next.js. De fetch wrapper genérico até cache, revalidação e tratamento de erro, tudo com TypeScript garantindo segurança de tipos.
O Problema do Fetch Sem Tipagem
O fetch nativo do JavaScript (e do Next.js) não sabe nada sobre o formato dos dados que a API retorna. Quando você faz const data = await response.json(), o TypeScript infere data como any. A partir daí, qualquer acesso é permitido: data.nome, data.xyz, data.o_que_quiser. Nenhum erro. Nenhum autocomplete.
No Next.js App Router, fetch ganhou superpoderes: cache automático, revalidação por tempo, revalidação por tag. Mas esses recursos também precisam de tipagem pra funcionar bem. As opções de next.revalidate, next.tags e cache estão no tipo RequestInit estendido do Next.js.
O caminho certo é criar um wrapper de fetch que aceita generics. Você passa o tipo esperado e o TypeScript garante que o retorno bate com o que a API promete. Se a API mudar, o build quebra no lugar certo.
Galera costuma fazer as any ou JSON.parse sem tipo pra resolver rápido. Funciona no momento, mas cria dívida técnica que cobra caro depois. Um wrapper tipado resolve de vez.
Passo a Passo: Fetch Tipado no Next.js
Vamos construir um sistema de data fetching tipado do zero. Cada passo adiciona segurança.
Exemplos Práticos de Fetch Tipado
Do wrapper básico ao avançado. Cada exemplo resolve um problema real.
Fetch Wrapper Genérico
// lib/api.ts
// Tipo de erro padronizado
type ApiError = {
message: string;
status: number;
};
// Tipo de resultado genérico
type ApiResult<T> =
| { data: T; error: null }
| { data: null; error: ApiError };
// Options estendidas do Next.js
type FetchOptions = RequestInit & {
next?: {
revalidate?: number | false;
tags?: string[];
};
};
const BASE_URL = process.env.API_URL ?? "https://api.example.com";
export async function api<T>(
endpoint: string,
options: FetchOptions = {}
): Promise<ApiResult<T>> {
try {
const response = await fetch(`${BASE_URL}${endpoint}`, {
headers: {
"Content-Type": "application/json",
...options.headers,
},
...options,
});
if (!response.ok) {
return {
data: null,
error: {
message: `API error: ${response.statusText}`,
status: response.status,
},
};
}
const data: T = await response.json();
return { data, error: null };
} catch (err) {
return {
data: null,
error: { message: "Network error", status: 0 },
};
}
}
Tipos da API Centralizados
// types/api.ts
export type Post = {
id: string;
title: string;
content: string;
slug: string;
author: Author;
tags: string[];
publishedAt: string;
updatedAt: string;
};
export type Author = {
id: string;
name: string;
avatar: string;
};
export type PaginatedResponse<T> = {
data: T[];
total: number;
page: number;
pageSize: number;
totalPages: number;
};
export type PostFilters = {
category?: string;
tag?: string;
page?: number;
limit?: number;
};
Fetch em Server Components com Cache
// app/blog/page.tsx
import { api } from "@/lib/api";
import type { Post, PaginatedResponse } from "@/types/api";
export default async function BlogPage() {
// Fetch tipado com cache de 1 hora e tag pra invalidação
const result = await api<PaginatedResponse<Post>>("/posts", {
next: {
revalidate: 3600,
tags: ["posts"],
},
});
if (result.error) {
return <p>Erro: {result.error.message}</p>;
}
// result.data tem tipo PaginatedResponse<Post>
const { data: posts, totalPages } = result.data;
return (
<div>
{posts.map((post) => (
// post tem autocomplete completo: title, slug, author...
<article key={post.id}>
<h2>{post.title}</h2>
<p>Por {post.author.name}</p>
</article>
))}
</div>
);
}
Fetch com Query Parameters Tipados
// lib/api.ts - versão com query params
function buildQuery(params: Record<string, string | number | undefined>): string {
const filtered = Object.entries(params)
.filter(([_, v]) => v !== undefined)
.map(([k, v]) => `${k}=${encodeURIComponent(String(v))}`);
return filtered.length ? `?${filtered.join("&")}` : "";
}
// Funções específicas por recurso
export async function getPosts(
filters: PostFilters = {}
): Promise<ApiResult<PaginatedResponse<Post>>> {
const query = buildQuery({
category: filters.category,
tag: filters.tag,
page: filters.page,
limit: filters.limit,
});
return api<PaginatedResponse<Post>>(`/posts${query}`, {
next: { revalidate: 3600, tags: ["posts"] },
});
}
export async function getPost(
slug: string
): Promise<ApiResult<Post>> {
return api<Post>(`/posts/${slug}`, {
next: { revalidate: 3600, tags: ["posts", `post-${slug}`] },
});
}
// Uso limpo no componente:
// const { data: posts } = await getPosts({ category: "react", page: 1 });
// const { data: post } = await getPost("meu-slug");
Fetch com Revalidação On-demand
// app/api/revalidate/route.ts
import { revalidateTag } from "next/cache";
import { NextRequest, NextResponse } from "next/server";
// Tags tipadas como union literal
type CacheTag = "posts" | "products" | "users" | `post-${string}`;
type RevalidateBody = {
tag: CacheTag;
secret: string;
};
export async function POST(request: NextRequest) {
const body: RevalidateBody = await request.json();
if (body.secret !== process.env.REVALIDATE_SECRET) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
// revalidateTag invalida todas as requests com essa tag
revalidateTag(body.tag);
return NextResponse.json({
revalidated: true,
tag: body.tag,
now: Date.now(),
});
}
// Chamada externa:
// POST /api/revalidate { tag: "posts", secret: "xyz" }
// Todos os fetches com tag "posts" são invalidados
Fetch no Client Component com SWR Tipado
// components/SearchResults.tsx
"use client";
import useSWR from "swr";
import type { Post } from "@/types/api";
// Fetcher tipado pra SWR
const fetcher = async <T,>(url: string): Promise<T> => {
const res = await fetch(url);
if (!res.ok) throw new Error("Fetch failed");
return res.json() as Promise<T>;
};
type SearchResultsProps = {
query: string;
};
export function SearchResults({ query }: SearchResultsProps) {
// SWR com generics: data tem tipo Post[]
const { data, error, isLoading } = useSWR<Post[]>(
query ? `/api/search?q=${query}` : null,
fetcher<Post[]>
);
if (isLoading) return <p>Buscando...</p>;
if (error) return <p>Erro na busca</p>;
if (!data?.length) return <p>Nenhum resultado</p>;
return (
<ul>
{data.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
O padrão é sempre o mesmo: defina o tipo, passe como generic, trate o retorno. Não importa se é Server Component com fetch nativo, Client Component com SWR ou route handler. O generic T é seu seguro contra dados inesperados.
Erros Comuns com Fetch Tipado
Armadilhas que passam batido
Confiar cegamente no tipo genérico: api<Post>('/posts') não valida que a API realmente retorna Post. O TypeScript confia em você. Se a API mudar e retornar { items: Post[] } ao invés de Post, o tipo não protege em runtime. Use Zod pra validação quando os dados são críticos.
Esquecer de tratar response.ok: fetch não lança erro pra status 4xx/5xx. Se você faz const data = await fetch(url).then(r => r.json()), um 404 retorna dados inesperados sem aviso. Sempre cheque response.ok antes de parsear.
Usar cache: 'force-cache' sem revalidate: Cache eterno significa que dados nunca atualizam. Sempre combine com next.revalidate ou next.tags pra ter controle sobre quando o cache expira.
Tipar response.json() como any e fazer as Type: Isso é type assertion, não tipagem. O TypeScript não valida nada. Use generics no wrapper pra manter consistência. Evite as ao máximo.
Fetch no Client Component sem loading/error: No server, se fetch falha o error.tsx captura. No client, você precisa tratar manualmente. Use SWR ou React Query que gerenciam loading, error e cache automaticamente.
Checklist de Fetch Tipado no Next.js
Data Fetching Profissional no CrazyStack
Fetch tipado é o coração de qualquer app Next.js sério. No CrazyStack, você constrói um sistema completo de data fetching: wrapper genérico, cache inteligente, revalidação on-demand, loading states e error boundaries. Tudo integrado num SaaS real com TypeScript de ponta a ponta.
Se você quer parar de fazer fetch sem tipo e começar a ter controle total sobre os dados do seu app, esse projeto te leva lá.
Continue lendo
Como Tipar Server Actions no Next.js com TypeScript
Tipagem de Server Actions com 'use server', form data e retornos tipados no Next.js.
Como Criar API no Next.js com TypeScript
Route Handlers tipados no Next.js App Router com validação e retornos seguros.
Como Usar TypeScript com Next.js App Router
Setup completo de TypeScript no Next.js App Router: page, layout, loading, error e config.
Interface no TypeScript
Quando usar interface