Alternar tema

Busca de dados em Server Components do Next.js: fetch, banco de dados e boas práticas

Easton editorial illustration: route-map drafting table

Quando escrevi um componente no App Router do Next.js pela primeira vez, encontrei um código assim:

async function Page() {
  const data = await fetch('...')
  return <div>{data}</div>
}

Só isso? await direto? Sem useEffect, sem useState e sem se preocupar com condições de corrida?

Foi estranho no começo. Depois de se acostumar ao modelo do React no cliente, ouvir que “um componente pode ser assíncrono” parece uma mudança nas regras. E ainda surge outra dúvida: usar a API fetch ou consultar o banco diretamente? fetch acrescenta uma chamada de API; uma consulta direta parece correr o risco de expor as credenciais do banco ao cliente.

Se essas dúvidas também são suas, este texto explica como fazer busca de dados em Server Components do Next.js: quando usar fetch, quando consultar o banco, como escrever componentes assíncronos, controlar o cache e evitar as armadilhas mais comuns.

Fundamentos da busca de dados em Server Components

Por que Server Components podem usar await diretamente?

A resposta curta: Server Components rodam no servidor, não no navegador.

Essa distinção é essencial. Componentes React tradicionais são renderizados no navegador e não acessam diretamente o banco de dados ou o sistema de arquivos. Server Components são executados no servidor e, por isso, podem fazer tarefas antes restritas às rotas de API:

  • Conectar-se diretamente ao banco de dados, com Prisma, Drizzle ou SQL nativo
  • Ler o sistema de arquivos, por exemplo, arquivos Markdown
  • Chamar serviços internos sem se preocupar com origem cruzada
  • Acessar variáveis de ambiente e segredos sem expô-los ao cliente

Por isso, este código é seguro:

// app/posts/page.tsx
import { db } from '@/lib/db'

async function PostsPage() {
  // Consulta direta; as credenciais não são enviadas ao navegador
  const posts = await db.post.findMany()

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

export default PostsPage

Observe quatro pontos:

  1. O componente é declarado como async function.
  2. A consulta ao banco pode ser usada diretamente com await.
  3. Hooks do React, como useState e useEffect, não podem ser usados.
  4. Por padrão, o componente é renderizado no servidor e o cliente recebe apenas HTML.

Três formas principais de buscar dados

Em Server Components, há três opções.

1. API fetch

É a forma mais conhecida, indicada para chamar APIs externas ou um Route Handler próprio:

async function Page() {
  const res = await fetch('https://api.example.com/data')
  const data = await res.json()
  return <div>{data.title}</div>
}

2. Consulta direta ao banco de dados

Use um ORM ou cliente de banco diretamente:

import { db } from '@/lib/db'

async function Page() {
  const data = await db.posts.findFirst()
  return <div>{data.title}</div>
}

3. Server Actions

São usadas para modificar dados, como ao enviar um formulário ou excluir um registro; além de ler, elas podem gravar dados:

async function createPost(formData: FormData) {
  'use server'
  const title = formData.get('title')
  await db.post.create({ data: { title } })
}

Qual escolher? É o que veremos a seguir.

fetch ou consulta ao banco: como escolher?

Essa era minha maior dúvida quando comecei a usar o App Router. Depois de alguma prática, a decisão ficou bem clara.

Árvore de decisão: escolha em cinco segundos

Faça três perguntas:

  1. É um Server Component? Se sim, continue; se for um Client Component, vá para a terceira pergunta.
  2. De onde vêm os dados?
    • Do seu próprio banco → consulte o banco diretamente.
    • De uma API de terceiros → use fetch.
  3. Um Client Component precisa buscar os dados? Crie uma rota de API e use fetch.

É só isso.

Por que dar preferência à consulta direta ao banco?

A recomendação oficial do Next.js é clara: em um Server Component, não faça um desvio por uma rota de API; consulte a fonte diretamente.

Há bons motivos para isso.

1. Elimina uma ida e volta HTTP

Compare:

// ❌ Caminho longo: Server Component → API Route → Database
async function Page() {
  const res = await fetch('/api/posts')  // Chamada HTTP
  const posts = await res.json()
  return <PostList posts={posts} />
}

// ✅ Caminho direto: Server Component → Database
async function Page() {
  const posts = await db.post.findMany()  // Consulta direta
  return <PostList posts={posts} />
}

A segunda opção remove uma camada e responde mais rápido. Pode parecer uma diferença pequena, mas reduzir 100 a 200 ms no carregamento de uma página é perceptível.

2. Melhora a segurança de tipos

Com TypeScript e Prisma ou Drizzle, a consulta direta oferece inferência completa de tipos:

// Tipos inferidos automaticamente e bom autocomplete no editor
const post = await db.post.findFirst({
  include: { author: true, comments: true }
})

// post.author.name ← Tem sugestão de tipo
// post.comments[0].content ← Também tem

Com fetch, você precisa definir os tipos manualmente ou usar uma asserção com as, o que facilita erros.

3. Deixa o código mais simples

Não é preciso criar outro arquivo de API nem tratar status HTTP e erros nessa camada. O volume de código cai quase pela metade.

Quando API e fetch são necessários?

Isso não significa abandonar rotas de API. Elas ainda são necessárias em três situações.

Situação 1: um Client Component precisa dos dados

Um componente do cliente não pode consultar o banco diretamente, pois roda no navegador. Nesse caso, crie um endpoint:

// app/api/posts/route.ts
export async function GET() {
  const posts = await db.post.findMany()
  return Response.json(posts)
}

// components/client-posts.tsx
'use client'
export function ClientPosts() {
  const [posts, setPosts] = useState([])

  useEffect(() => {
    fetch('/api/posts')
      .then(res => res.json())
      .then(setPosts)
  }, [])

  return <div>{/* Renderiza posts */}</div>
}

Situação 2: você precisa expor uma API

Se o aplicativo Next.js precisa fornecer dados a outros serviços, como um app móvel ou um terceiro, crie um endpoint público.

Situação 3: integração com um serviço de terceiros

Para chamar GitHub API, OpenAI API e serviços semelhantes, use fetch:

async function Page() {
  const res = await fetch('https://api.github.com/users/vercel')
  const user = await res.json()
  return <div>Seguidores: {user.followers}</div>
}

Como escrever componentes com async/await

Depois de escolher a fonte, resta escrever o componente.

Padrão básico: simples de verdade

Um componente assíncrono básico tem esta forma:

async function ProductPage({ params }: { params: { id: string } }) {
  const product = await db.product.findUnique({
    where: { id: params.id }
  })

  if (!product) {
    return <div>Produto não encontrado</div>
  }

  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.price}</p>
    </div>
  )
}

Não há estado de carregamento nem useEffect: aguarde os dados e renderize.

Paralelo ou sequencial: uma grande diferença de desempenho

Esta é uma armadilha em que já caí. Qual dos dois códigos abaixo é mais rápido?

// ❌ Sequencial: lento
async function Page() {
  const user = await db.user.findFirst()      // Aguarda 200 ms
  const posts = await db.post.findMany()      // Depois, mais 150 ms
  const comments = await db.comment.findMany()  // Depois, mais 100 ms
  // Total: 450 ms
  return <Dashboard user={user} posts={posts} comments={comments} />
}

// ✅ Paralelo: rápido
async function Page() {
  const [user, posts, comments] = await Promise.all([
    db.user.findFirst(),       // Inicia ao mesmo tempo
    db.post.findMany(),        // Inicia ao mesmo tempo
    db.comment.findMany(),     // Inicia ao mesmo tempo
  ])
  // Total: 200 ms, o tempo da operação mais lenta
  return <Dashboard user={user} posts={posts} comments={comments} />
}

A diferença passa de duas vezes. Quando os dados não dependem uns dos outros, use Promise.all para buscá-los em paralelo.

Quando há dependência, a execução precisa ser sequencial:

// Precisa ser sequencial: a segunda consulta depende da primeira
async function Page({ params }) {
  const user = await db.user.findUnique({ where: { id: params.id } })
  // Primeiro obtemos user; depois buscamos os posts desse usuário
  const posts = await db.post.findMany({ where: { authorId: user.id } })
  return <Profile user={user} posts={posts} />
}

Limites de Suspense: controle a experiência de carregamento

Você pode pensar: “Se os dados são buscados no servidor, o usuário só vê uma tela em branco?”

Isso pode acontecer, mas o Next.js oferece loading.js e Suspense para melhorar a experiência.

Método 1: arquivo loading.js

Crie loading.tsx na pasta da rota e ele será aplicado automaticamente:

// app/posts/loading.tsx
export default function Loading() {
  return <div>Carregando posts...</div>
}

// app/posts/page.tsx
async function PostsPage() {
  const posts = await db.post.findMany()  // Consulta lenta
  return <PostList posts={posts} />
}

O usuário vê primeiro “Carregando posts…” e, quando os dados chegam, o conteúdo real ocupa o lugar.

Método 2: Suspense manual

Para ter controle mais preciso, envolva o componente com Suspense:

import { Suspense } from 'react'

async function SlowComponent() {
  const data = await slowQuery()  // 3 segundos
  return <div>{data}</div>
}

async function FastComponent() {
  const data = await fastQuery()  // 0,5 segundo
  return <div>{data}</div>
}

export default function Page() {
  return (
    <div>
      <FastComponent />  {/* O rápido aparece primeiro */}
      <Suspense fallback={<div>Carregando...</div>}>
        <SlowComponent />  {/* O lento aguarda sem bloquear o conteúdo acima */}
      </Suspense>
    </div>
  )
}

Erro comum: colocar Suspense no lugar errado

Eu também já cometi este erro:

// ❌ Incorreto: Suspense dentro do componente async não funciona
async function Page() {
  return (
    <Suspense fallback={<div>Carregando...</div>}>
      {await slowQuery()}  {/* Suspense não consegue interceptar */}
    </Suspense>
  )
}

// ✅ Correto: Suspense envolve o componente async por fora
export default function Layout() {
  return (
    <Suspense fallback={<div>Carregando...</div>}>
      <SlowPage />  {/* Componente async */}
    </Suspense>
  )
}

Suspense precisa ficar fora do componente assíncrono para capturar a promise.

Deduplicação automática de requisições

Outro recurso interessante: quando a mesma requisição é feita várias vezes durante uma renderização, o Next.js elimina as duplicatas automaticamente.

async function Header() {
  const user = await db.user.findFirst()  // Consulta 1
  return <div>{user.name}</div>
}

async function Sidebar() {
  const user = await db.user.findFirst()  // Consulta 2, mas não será executada de fato
  return <div>{user.name}</div>
}

export default function Page() {
  return (
    <div>
      <Header />
      <Sidebar />
      {/* Na prática, apenas uma consulta é executada */}
    </div>
  )
}

O Next.js guarda o primeiro resultado e devolve o mesmo valor nas chamadas seguintes. Assim, vários componentes podem chamar a mesma fonte de dados sem acrescentar trabalho desnecessário.

Estratégias de cache e revalidação de dados

O Next.js 15 trouxe uma mudança importante no cache, e ela pode causar surpresas.

O padrão de cache mudou no Next.js 15

Antes, no Next.js 14: fetch usava cache: 'force-cache' por padrão e mantinha os dados em cache.

Agora, no Next.js 15: fetch usa cache: 'no-store' por padrão; não há cache e os dados são buscados novamente a cada vez.

Por que mudar? O comportamento anterior levava muita gente a esperar dados atualizados e receber dados antigos do cache. Não armazenar por padrão é mais explícito.

Ao migrar da versão 14 para a 15, uma página pode ficar mais lenta porque uma API antes armazenada passa a ser chamada em toda requisição.

Três estratégias de cache

Escolha conforme as características dos dados.

1. Cache completo, para conteúdo estático

async function BlogPost({ slug }) {
  const post = await fetch(`https://api.example.com/posts/${slug}`, {
    cache: 'force-cache'  // Cache permanente até uma nova compilação
  })
  return <article>{post.content}</article>
}

É indicado para posts, páginas de produto e documentação, cujo conteúdo muda pouco.

2. Sem cache, para dados em tempo real

async function StockPrice() {
  const price = await fetch('https://api.example.com/stock', {
    cache: 'no-store'  // Busca novamente em toda requisição
  })
  return <div>Preço atual: {price}</div>
}

É indicado para preços de ações, comentários em tempo real e estado do usuário, que precisam estar atualizados.

3. Revalidação periódica com ISR

async function ProductList() {
  const products = await fetch('https://api.example.com/products', {
    next: { revalidate: 60 }  // Expira após 60 segundos e busca novamente
  })
  return <div>{products.map(p => <Card key={p.id} {...p} />)}</div>
}

É indicado para listas de produtos e páginas iniciais de notícias, que toleram algumas dezenas de segundos de atraso, mas não dados muito antigos.

Revalidação manual: atualize assim que os dados mudarem

Quando uma alteração, como a publicação de um post, exige atualizar o cache imediatamente, o Next.js oferece duas APIs.

1. revalidatePath, para atualizar uma página inteira

'use server'
import { revalidatePath } from 'next/cache'

async function createPost(formData: FormData) {
  await db.post.create({ data: {...} })
  revalidatePath('/posts')  // Atualiza o cache da página /posts
}

2. revalidateTag, para atualizar uma tag específica

Ela oferece controle mais preciso:

// Adiciona uma tag ao buscar os dados
async function getPosts() {
  const res = await fetch('https://api.example.com/posts', {
    next: { tags: ['posts'] }  // Adiciona a tag 'posts'
  })
  return res.json()
}

// Atualiza a tag quando necessário
'use server'
import { revalidateTag } from 'next/cache'

async function createPost() {
  await db.post.create({ data: {...} })
  revalidateTag('posts')  // Atualiza apenas o cache marcado com 'posts'
}

Tratamento de erros e otimização de desempenho

Tratamento de erros: evite derrubar a página

Por padrão, uma falha ao buscar dados em um Server Component interrompe a página inteira. É preciso tratar o erro.

Método 1: try/catch

async function Page() {
  try {
    const data = await fetch('https://api.example.com/data')
    if (!data.ok) throw new Error('Failed to fetch')
    return <div>{data.title}</div>
  } catch (error) {
    return <div>Algo deu errado. Tente novamente.</div>
  }
}

Método 2: arquivo error.js

Crie error.tsx na pasta da rota para capturar automaticamente erros dessa rota e de suas sub-rotas:

// app/posts/error.tsx
'use client'  // O limite de erro precisa ser um Client Component

export default function Error({
  error,
  reset,
}: {
  error: Error
  reset: () => void
}) {
  return (
    <div>
      <h2>Algo deu errado!</h2>
      <button onClick={() => reset()}>Tentar novamente</button>
    </div>
  )
}

Uma armadilha: redirect é interceptado dentro de try/catch

// ❌ Incorreto: o erro lançado por redirect é capturado
async function Page() {
  try {
    const user = await getUser()
    if (!user) redirect('/login')  // O erro é capturado pelo catch abaixo
  } catch (error) {
    return <div>Erro</div>  // O redirecionamento não acontece
  }
}

// ✅ Correto: redirect fica fora de try/catch
async function Page() {
  let user
  try {
    user = await getUser()
  } catch (error) {
    return <div>Erro</div>
  }

  if (!user) redirect('/login')  // Agora o redirecionamento funciona
}

Erros comuns e soluções

Aqui estão algumas armadilhas que já encontrei.

Erro 1: usar caminho relativo em fetch no servidor

// ❌ Incorreto: o servidor não tem uma URL base
async function Page() {
  const data = await fetch('/api/posts')  // Erro
}

// ✅ Correto: use uma URL absoluta
async function Page() {
  const data = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/posts`)
}

// ✅ Melhor: consulte o banco diretamente, sem fetch
async function Page() {
  const posts = await db.post.findMany()
}

Erro 2: esquecer de verificar response.ok

// ❌ Incorreto: fetch não lança erros automaticamente
async function Page() {
  const res = await fetch('https://api.example.com/data')
  const data = await res.json()  // Em um 404, isto pode falhar
  return <div>{data.title}</div>
}

// ✅ Correto: verifique o status
async function Page() {
  const res = await fetch('https://api.example.com/data')

  if (!res.ok) {
    throw new Error(`HTTP error! status: ${res.status}`)
  }

  const data = await res.json()
  return <div>{data.title}</div>
}

Erro 3: chamar um Route Handler em um Server Component

// ❌ Não recomendado: é uma volta desnecessária
async function Page() {
  const res = await fetch('/api/posts')  // Por que percorrer essa camada?
  const posts = await res.json()
  return <PostList posts={posts} />
}

// ✅ Recomendado: consulte diretamente
async function Page() {
  const posts = await db.post.findMany()
  return <PostList posts={posts} />
}

Exemplo prático: criando uma página de blog

Depois da teoria, vejamos um exemplo completo.

Suponha que a página de detalhes de um post precise:

  • Exibir o conteúdo do post
  • Exibir informações do autor
  • Exibir recomendações de posts relacionados

Estrutura de arquivos

app/
  posts/
    [slug]/
      page.tsx       ← Página de detalhes do post
      loading.tsx    ← Estado de carregamento
      error.tsx      ← Tratamento de erros

Implementação

// app/posts/[slug]/page.tsx
import { db } from '@/lib/prisma'
import { Suspense } from 'react'
import { notFound } from 'next/navigation'

// Componente principal da página
export default async function PostPage({
  params,
}: {
  params: { slug: string }
}) {
  // Busca o post e o autor em paralelo
  const [post, author] = await Promise.all([
    db.post.findUnique({
      where: { slug: params.slug },
    }),
    db.user.findFirst(),
  ])

  if (!post) {
    notFound()  // Exibe a página 404
  }

  return (
    <article>
      <h1>{post.title}</h1>
      <AuthorCard author={author} />
      <div>{post.content}</div>

      {/* Recomendações podem carregar depois, sem bloquear o conteúdo principal */}
      <Suspense fallback={<div>Carregando recomendações...</div>}>
        <RecommendedPosts currentPostId={post.id} />
      </Suspense>
    </article>
  )
}

// Cartão do autor; os dados já estão disponíveis
function AuthorCard({ author }) {
  return (
    <div>
      <img src={author.avatar} alt={author.name} />
      <span>{author.name}</span>
    </div>
  )
}

// Posts recomendados: componente assíncrono com carregamento independente
async function RecommendedPosts({ currentPostId }: { currentPostId: string }) {
  const recommended = await db.post.findMany({
    where: {
      id: { not: currentPostId },
      published: true,
    },
    take: 3,
  })

  return (
    <div>
      <h3>Você também pode gostar</h3>
      {recommended.map((post) => (
        <a key={post.id} href={`/posts/${post.slug}`}>
          {post.title}
        </a>
      ))}
    </div>
  )
}

// Cache: revalida o conteúdo do post a cada hora
export const revalidate = 3600

// Gera parâmetros estáticos, opcionalmente, para geração estática
export async function generateStaticParams() {
  const posts = await db.post.findMany({
    select: { slug: true },
  })

  return posts.map((post) => ({
    slug: post.slug,
  }))
}
// app/posts/[slug]/loading.tsx
export default function Loading() {
  return (
    <div>
      <div className="skeleton h-12 w-3/4" />
      <div className="skeleton h-4 w-1/4 mt-4" />
      <div className="skeleton h-64 mt-8" />
    </div>
  )
}
// app/posts/[slug]/error.tsx
'use client'

export default function Error({
  error,
  reset,
}: {
  error: Error
  reset: () => void
}) {
  return (
    <div>
      <h2>Falha ao carregar o post</h2>
      <p>{error.message}</p>
      <button onClick={() => reset()}>Tentar novamente</button>
    </div>
  )
}

Por que essas decisões foram tomadas?

  1. Por que consultar o banco diretamente? É um Server Component, então não há motivo para passar por uma rota de API.
  2. Por que buscar post e autor em paralelo? As consultas são independentes; em paralelo, terminam mais rápido.
  3. Por que usar Suspense para as recomendações? Elas são secundárias e podem carregar depois sem bloquear o conteúdo principal.
  4. Por que usar revalidate: 3600? O conteúdo muda pouco; uma hora de cache reduz a carga no banco sem deixá-lo muito desatualizado.

Conclusão

Os pontos centrais são estes:

  1. Em Server Components, prefira consultar o banco diretamente, exceto quando um Client Component precisar dos dados ou quando houver integração com uma API de terceiros.
  2. Componentes com async/await são simples, mas use Promise.all para paralelizar consultas e Suspense para melhorar o carregamento.
  3. O Next.js 15 não usa cache por padrão; escolha force-cache, no-store ou revalidate conforme os dados.
  4. Trate os erros: verifique response.ok, use error.tsx como fallback e mantenha redirect fora de try/catch.

Buscar dados em Server Components é bem mais simples do que no cliente. Você não precisa administrar manualmente o estado de carregamento, condições de corrida ou cancelamento de requisições. Se ainda está em dúvida sobre migrar para o App Router, esse ganho já é um bom motivo para experimentar.

No próximo projeto, vale testar essa abordagem. Se surgir alguma dúvida, volte a este guia.

Fluxo completo de busca de dados em Server Components do Next.js

Da escolha entre fetch e banco de dados até componentes assíncronos, cache e tratamento de erros

⏱️ Estimated time: 1 hr

  1. 1

    Step 1: Escolher entre fetch e consulta ao banco

    Consulta direta ao banco (recomendada):
    • Mais rápida: elimina uma chamada de API e reduz a latência
    • Mais segura: as credenciais do banco não chegam ao cliente
    • Indicada quando: o banco está acessível no servidor

    Exemplo:
    ```tsx
    import { db } from '@/lib/db'

    export default async function Page() {
    const users = await db.user.findMany()
    return <div>{users.map(u => <div key={u.id}>{u.name}</div>)}</div>
    }
    ```

    Uso de fetch:
    • Indicado para APIs de terceiros
    • Indicado para cenários entre origens
    • Atenção: o Next.js 15 não usa cache por padrão

    Exemplo:
    ```tsx
    export default async function Page() {
    const res = await fetch('https://api.example.com/data', {
    cache: 'force-cache' // No Next.js 15, configure explicitamente
    })
    const data = await res.json()
    return <div>{data}</div>
    }
    ```

    Recomendação: prefira consultar o banco diretamente e use fetch quando precisar integrar uma API de terceiros.
  2. 2

    Step 2: Escrever componentes com async/await

    Marque o componente como async:
    ```tsx
    export default async function Page() {
    const data = await fetchData()
    return <div>{data}</div>
    }
    ```

    Busca paralela com Promise.all:
    ```tsx
    export default async function Page() {
    const [users, posts] = await Promise.all([
    fetchUsers(),
    fetchPosts()
    ])
    return <div>...</div>
    }
    ```

    Melhore o carregamento com Suspense:
    ```tsx
    import { Suspense } from 'react'

    export default function Page() {
    return (
    <Suspense fallback={<div>Loading...</div>}>
    <UserList />
    </Suspense>
    )
    }

    async function UserList() {
    const users = await fetchUsers()
    return <div>{users.map(...)}</div>
    }
    ```

    Pontos principais:
    • Server Components podem ser async
    • Use Promise.all para buscar em paralelo
    • Use Suspense para melhorar a experiência de carregamento
  3. 3

    Step 3: Configurar a estratégia de cache

    O Next.js 15 não usa cache por padrão, então configure explicitamente:

    Sem cache, para dados em tempo real:
    ```tsx
    fetch(url, { cache: 'no-store' })
    ```

    Cache permanente, para dados estáticos:
    ```tsx
    fetch(url, { cache: 'force-cache' })
    ```

    Atualização periódica com ISR:
    ```tsx
    fetch(url, { next: { revalidate: 3600 } })
    ```

    Escolha:
    • Dados em tempo real → cache: 'no-store'
    • Dados estáticos → cache: 'force-cache'
    • Atualizações frequentes sem exigência de tempo real → revalidate

    A filosofia do Next.js 15 é tornar o comportamento explícito: decida ativamente quais dados precisam de cache.
  4. 4

    Step 4: Tratar erros

    Verifique response.ok:
    ```tsx
    const res = await fetch(url)
    if (!res.ok) {
    throw new Error('Failed to fetch')
    }
    const data = await res.json()
    ```

    Use error.tsx como fallback:
    ```tsx
    // app/page/error.tsx
    'use client'
    export default function Error({ error, reset }) {
    return (
    <div>
    <h2>Ocorreu um erro: {error.message}</h2>
    <button onClick={reset}>Tentar novamente</button>
    </div>
    )
    }
    ```

    Atenção ao redirect:
    ```tsx
    // ❌ Incorreto: redirect dentro de try/catch
    try {
    if (!user) redirect('/login')
    } catch (e) {
    // redirect lança um erro, que será capturado aqui
    }

    // ✅ Correto: redirect fora de try/catch
    if (!user) redirect('/login')
    try {
    // Outra lógica
    } catch (e) {
    // Tratamento de erros
    }
    ```

    Pontos principais:
    • Verifique response.ok
    • Use error.tsx como fallback
    • Não coloque redirect dentro de try/catch

FAQ

Por que Server Components podem usar await diretamente?
Porque Server Components rodam no servidor, não no navegador.

Eles podem:
• Conectar-se diretamente ao banco de dados, com Prisma, Drizzle ou SQL nativo
• Ler o sistema de arquivos, como arquivos Markdown
• Chamar serviços internos sem problemas de origem cruzada
• Acessar variáveis de ambiente e segredos sem expô-los ao cliente

Portanto, este código é seguro:
```tsx
import { db } from '@/lib/db'

export default async function Page() {
const users = await db.user.findMany() // A credencial do banco não é exposta
return <div>{users.map(...)}</div>
}
```

Vantagens:
• Dispensa useEffect e useState
• Evita preocupações com condições de corrida
• Dispensa o gerenciamento manual do estado de carregamento
• A busca de dados fica bem mais simples do que no cliente
Quando usar fetch e quando consultar o banco diretamente?
Consulta direta ao banco (recomendada):
• Mais rápida: elimina uma chamada de API e reduz a latência; servidor-banco costuma ficar abaixo de 10 ms, enquanto cliente-servidor pode passar de 100 ms
• Mais segura: as credenciais do banco não chegam ao cliente
• Indicada quando: o banco está acessível no servidor

Use fetch quando:
• For integrar uma API de terceiros
• Houver uma necessidade entre origens
• A API já existir e você não quiser alterar a arquitetura

Recomendação:
• Prefira consultar o banco diretamente
• Use fetch quando precisar integrar uma API de terceiros
• Evite chamar a própria API dentro de um Server Component

Comparação:
```tsx
// ❌ Antipadrão: chamar a própria API em um Server Component
const res = await fetch('/api/users')
const users = await res.json()

// ✅ Correto: consultar o banco diretamente
const users = await db.user.findMany()
```
Como escrever um componente async?
Marque o componente como async:
```tsx
export default async function Page() {
const data = await fetchData()
return <div>{data}</div>
}
```

Busca paralela com Promise.all:
```tsx
export default async function Page() {
const [users, posts] = await Promise.all([
fetchUsers(),
fetchPosts()
])
return <div>...</div>
}
```

Melhore o carregamento com Suspense:
```tsx
import { Suspense } from 'react'

export default function Page() {
return (
<Suspense fallback={<div>Loading...</div>}>
<UserList />
</Suspense>
)
}

async function UserList() {
const users = await fetchUsers()
return <div>{users.map(...)}</div>
}
```

Pontos principais:
• Server Components podem ser async
• Use Promise.all para buscar em paralelo
• Use Suspense para melhorar o carregamento
Como configurar o cache no Next.js 15?
O Next.js 15 não usa cache por padrão, então configure explicitamente:

Sem cache, para dados em tempo real:
```tsx
fetch(url, { cache: 'no-store' })
```

Cache permanente, para dados estáticos:
```tsx
fetch(url, { cache: 'force-cache' })
```

Atualização periódica com ISR:
```tsx
fetch(url, { next: { revalidate: 3600 } })
```

Escolha:
• Dados em tempo real → cache: 'no-store'
• Dados estáticos → cache: 'force-cache'
• Atualizações frequentes sem exigência de tempo real → revalidate

A filosofia do Next.js 15 é tornar o comportamento explícito. Essa é uma mudança incompatível que merece atenção durante a migração.
Como tratar erros em Server Components?
Verifique response.ok:
```tsx
const res = await fetch(url)
if (!res.ok) {
throw new Error('Failed to fetch')
}
const data = await res.json()
```

Use error.tsx como fallback:
```tsx
// app/page/error.tsx
'use client'
export default function Error({ error, reset }) {
return (
<div>
<h2>Ocorreu um erro: {error.message}</h2>
<button onClick={reset}>Tentar novamente</button>
</div>
)
}
```

Atenção ao redirect:
```tsx
// ❌ Incorreto: redirect dentro de try/catch
try {
if (!user) redirect('/login')
} catch (e) {
// redirect lança um erro, que será capturado aqui
}

// ✅ Correto: redirect fora de try/catch
if (!user) redirect('/login')
try {
// Outra lógica
} catch (e) {
// Tratamento de erros
}
```

Pontos principais:
• Verifique response.ok
• Use error.tsx como fallback
• Não coloque redirect dentro de try/catch, pois redirect lança um erro

15 min de leitura · Publicado em: 19 dez 2025 · Atualizado em: 8 set 2026

Comentários

Entre com GitHub para comentar

Easton BlogEaston Blog