DevTools

Cheatsheet Next.js

Framework React full-stack com renderização no servidor e App Router

Voltar às linguagens
Next.js
85 cards encontrados
Categorias:
Versões:

Setup e CLI


9 cards
Criar projecto
npx create-next-app@latest meu-app
cd meu-app
npm run dev

create-next-app gera um projecto completo com App Router, TypeScript e ESLint. npm run dev inicia o servidor de desenvolvimento em localhost:3000 com hot reload.

TypeScript
// app/page.tsx
export default function Home() {
  return <h1>Olá</h1>;
}

// Tipos para params:
export default function Post({
  params,
}: { params: { slug: string } }) {}

Next.js tem suporte nativo a TypeScript. Ficheiros .tsx para componentes com JSX. O tsconfig.json é gerado automaticamente. Types para params e searchParams são inferidos.

ESLint e Prettier
// .eslintrc.json
{
  "extends": "next/core-web-vitals"
}

// next lint detecta:
// - hooks rules
// - image optimization
// - link issues

next/core-web-vitals inclui regras React + Next.js. Detecta uso incorrecto de Image, Link e hooks. npm run lint verifica o projecto inteiro.

Comandos CLI
npm run dev      # desenvolvimento
npm run build    # build de produção
npm run start    # servir build
npm run lint     # verificar código

dev para desenvolvimento com HMR. build compila e optimiza para produção. start serve o build. lint corre o ESLint configurado.

Variáveis de ambiente
// .env.local
DATABASE_URL=postgres://...
NEXT_PUBLIC_API_URL=https://api.exemplo.com

// No código:
process.env.DATABASE_URL       // servidor
process.env.NEXT_PUBLIC_API_URL // cliente

.env.local guarda segredos (não vai para git). Variáveis com prefixo NEXT_PUBLIC_ ficam disponíveis no cliente. Sem o prefixo, só acessíveis no servidor.

Estrutura App Router
app/
  layout.tsx      # layout raiz
  page.tsx        # página inicial (/)
  globals.css     # estilos globais
public/           # ficheiros estáticos
next.config.js    # configuração

A pasta app/ define as rotas por ficheiros. layout.tsx envolve todas as páginas. public/ serve ficheiros estáticos na raiz. next.config.js personaliza o build.

Path aliases
// tsconfig.json
{
  "compilerOptions": {
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}

// Uso:
import { Card } from "@/components/Card";

@/ é o alias padrão para src/ (ou raiz). Configurado em tsconfig.json com paths. Evita imports relativos longos como ../../../components.

next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  images: {
    domains: ["exemplo.com"],
  },
};

module.exports = nextConfig;

next.config.js configura o framework. reactStrictMode activa verificações extra. images.domains autoriza domínios externos para o componente Image.

src directory
src/
  app/
    layout.tsx
    page.tsx
  components/
    Card.tsx
  lib/
    utils.ts

A pasta src/ é opcional — organiza o código fonte separado de configs. Se existir, o App Router procura src/app/ em vez de app/. Recomendado para projectos grandes.

App Router e Páginas


10 cards
Página básica
// app/sobre/page.tsx
export default function Sobre() {
  return <h1>Sobre nós</h1>;
}

Cada pasta com page.tsx torna-se uma rota. app/sobre/page.tsx/sobre. O componente é exportado como default. Por defeito, é um Server Component.

loading.tsx
// app/blog/loading.tsx
export default function Loading() {
  return <p>A carregar posts...</p>;
}

loading.tsx mostra um fallback enquanto a página carrega. Envolve a página em Suspense automaticamente. Aparece instantaneamente na navegação — melhora a UX percebida.

Route groups
app/
  (marketing)/
    layout.tsx
    sobre/page.tsx
  (app)/
    layout.tsx
    dashboard/page.tsx

Parênteses (nome) criam grupos de rotas sem afectar o URL. Cada grupo pode ter o seu próprio layout. /sobre e /dashboard ficam em layouts diferentes sem prefixo na URL.

Rota raiz
// app/page.tsx
export default function Home() {
  return (
    <main>
      <h1>Bem-vindo</h1>
    </main>
  );
}

app/page.tsx é a página inicial (/). É o ponto de entrada da aplicação. Herda o layout raiz de app/layout.tsx automaticamente.

error.tsx
"use client";

// app/blog/error.tsx
export default function Error({
  error,
  reset,
}: { error: Error; reset: () => void }) {
  return (
    <div>
      <p>Erro: {error.message}</p>
      <button onClick={reset}>Tentar</button>
    </div>
  );
}

error.tsx captura erros de runtime na secção. Deve ser Client Component ("use client"). reset() tenta re-renderizar. Funciona como error boundary automático.

Parallel e intercepting routes
app/
  @modal/
    (.)photo/[id]/page.tsx
  layout.tsx    # recebe {modal, children}
  page.tsx

// layout.tsx:
export default function Layout({
  children, modal
}) { return <>{children}{modal}</> }

@slot define parallel routes (múltiplas páginas no mesmo layout). (.) intercepta rotas (modal sobre a página actual). Padrão para modais tipo Instagram/Twitter.

Layout raiz
// app/layout.tsx
export default function RootLayout({
  children,
}: { children: React.ReactNode }) {
  return (
    <html lang="pt">
      <body>{children}</body>
    </html>
  );
}

O layout raiz é obrigatório e envolve todas as páginas. Deve conter <html> e <body>. children é a página ou layout filho. Persiste entre navegações (sem re-mount).

not-found.tsx
// app/not-found.tsx
export default function NotFound() {
  return (
    <div>
      <h1>404</h1>
      <p>Página não encontrada</p>
    </div>
  );
}

not-found.tsx renderiza quando uma rota não existe ou quando notFound() é chamado manualmente. Pode existir global ou por secção. Retorna status HTTP 404.

Layout aninhado
// app/blog/layout.tsx
export default function BlogLayout({
  children,
}: { children: React.ReactNode }) {
  return (
    <div>
      <nav>Menu do blog</nav>
      {children}
    </div>
  );
}

Layouts em sub-pastas aplicam-se a todas as rotas dessa secção. app/blog/layout.tsx envolve /blog e /blog/[slug]. Layouts aninham hierarquicamente sem re-render na navegação.

template.tsx
// app/template.tsx
export default function Template({
  children,
}: { children: React.ReactNode }) {
  return <div className="animar">{children}</div>;
}

template.tsx é como layout mas recria em cada navegação (novo mount). Não preserva estado. Útil para animações de entrada ou efeitos que devem reiniciar a cada rota.

Componentes e Layout


9 cards
Metadata estático
// app/layout.tsx ou page.tsx
export const metadata = {
  title: "Meu Site",
  description: "Descrição do site",
  openGraph: {
    title: "Meu Site",
    images: ["/og.png"],
  },
};

metadata define SEO e Open Graph. Exportado como constante em Server Components. Gera tags <title>, <meta> e og: automaticamente no <head>.

next/image
import Image from "next/image";

<Image
  src="/foto.jpg"
  width={800}
  height={600}
  alt="Descrição"
  priority
/>

next/image optimiza imagens: resize, lazy load, formatos modernos (WebP/AVIF). width/height evitam layout shift. priority desactiva lazy load para imagens above-the-fold.

global-error e sitemap
// app/global-error.tsx ("use client")
export default function GlobalError() {
  return <html><body><h1>Erro</h1></body></html>;
}

// app/sitemap.ts
export default function sitemap() {
  return [{ url: "https://site.com", lastModified: new Date() }];
}

global-error.tsx substitui o layout raiz em erros fatais. sitemap.ts gera /sitemap.xml dinamicamente. Ambos são ficheiros de convenção do App Router.

Metadata dinâmico
// app/blog/[slug]/page.tsx
export async function generateMetadata({
  params,
}: { params: { slug: string } }) {
  const post = await getPost(params.slug);
  return { title: post.titulo };
}

generateMetadata() gera metadados dinâmicos por página. Recebe params e searchParams. Permite buscar dados e definir title, description, etc. por rota.

next/link
import Link from "next/link";

<Link href="/sobre">Sobre</Link>
<Link href="/blog" prefetch={true}>Blog</Link>
<Link href={{ pathname: "/post", query: { id: 1 } }}>
  Post
</Link>

next/link faz navegação client-side sem reload. Prefetch automático de páginas visíveis. Aceita string ou objecto com pathname e query. Substitui <a> para rotas internas.

Componente partilhado
// components/Card.tsx
export function Card({ titulo, texto }: {
  titulo: string;
  texto: string;
}) {
  return (
    <div className="card">
      <h3>{titulo}</h3>
      <p>{texto}</p>
    </div>
  );
}

Componentes em components/ são reutilizáveis. Sem directiva, são Server Components por defeito. Adicione "use client" se precisar de interactividade (hooks, eventos).

next/script
import Script from "next/script";

<Script
  src="https://analytics.exemplo.com/script.js"
  strategy="afterInteractive"
/>

next/script optimiza carregamento de scripts externos. strategy: beforeInteractive, afterInteractive (default), lazyOnload. Evita bloquear a renderização.

next/font
import { Inter } from "next/font/google";

const inter = Inter({
  subsets: ["latin"],
  variable: "--font-inter",
});

// layout.tsx:
<body className={inter.variable}>

next/font optimiza fontes automaticamente (zero layout shift). Faz download no build e serve localmente. variable cria CSS variable para usar em Tailwind ou CSS custom.

Icon e manifest
app/
  icon.png        # favicon automático
  apple-icon.png  # iOS home screen
  manifest.ts     # PWA manifest

// manifest.ts:
export default {
  name: "Minha App",
  theme_color: "#000",
};

Ficheiros de convenção: icon.png gera favicon automaticamente. manifest.ts configura PWA. Sem necessidade de tags manuais no <head> — Next.js injecta tudo.

Data Fetching


10 cards
Fetch no servidor
// app/page.tsx (Server Component)
export default async function Page() {
  const res = await fetch("https://api.exemplo.com/posts");
  const posts = await res.json();

  return <ul>{posts.map(p => <li key={p.id}>{p.titulo}</li>)}</ul>;
}

Server Components podem ser async e usar fetch directamente. Os dados são buscados no servidor — nunca expostos ao cliente. Sem necessidade de useEffect ou estado.

generateStaticParams
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
  const posts = await getPosts();
  return posts.map(post => ({
    slug: post.slug,
  }));
}

generateStaticParams() pré-gera rotas dinâmicas no build. Retorna array de params. Cada objecto gera uma página estática. Sem isto, rotas dinâmicas são SSR por defeito.

loading com Skeleton
// app/blog/loading.tsx
export default function Loading() {
  return (
    <div className="skeleton">
      <div className="h-4 w-3/4 animate-pulse" />
      <div className="h-4 w-1/2 animate-pulse" />
    </div>
  );
}

loading.tsx é automaticamente embrulhado em Suspense. Mostra skeleton/placeholder durante data fetching. Cada secção pode ter o seu próprio loading para granularidade.

SSG (estático)
// Gerado no build (estático)
export const dynamic = "force-static";

export default async function Page() {
  const dados = await fetch(url);
  return <p>{dados.titulo}</p>;
}

force-static gera a página no build (SSG). O HTML é servido de CDN sem computação. Ideal para conteúdo que não muda. Por defeito, páginas sem dados dinâmicos já são estáticas.

revalidatePath
"use server";
import { revalidatePath } from "next/cache";

export async function publicar() {
  await db.save(post);
  revalidatePath("/blog");
  revalidatePath("/");
}

revalidatePath() invalida a cache de um caminho manualmente. A próxima visita regenera a página. Use em Server Actions após mutações para garantir dados frescos.

unstable_cache
import { unstable_cache } from "next/cache";

const getCachedPosts = unstable_cache(
  async () => db.posts.findMany(),
  ["posts"],
  { revalidate: 3600, tags: ["posts"] }
);

const posts = await getCachedPosts();

unstable_cache faz cache de qualquer função async (não só fetch). Aceita chave, revalidate e tags. Útil para queries de BD ou cálculos pesados que não precisam de fetch HTTP.

ISR (revalidação)
// Revalidar a cada 60 segundos
export const revalidate = 60;

// Ou por fetch:
const res = await fetch(url, {
  next: { revalidate: 3600 }
});

revalidate activa ISR (Incremental Static Regeneration). A página é estática mas regenera em background a cada N segundos. Combina performance de SSG com dados actualizados.

revalidateTag
// Fetch com tag:
fetch(url, { next: { tags: ["posts"] } })

// Invalidar por tag:
import { revalidateTag } from "next/cache";
revalidateTag("posts");

tags agrupam fetches relacionados. revalidateTag() invalida todos os fetches com essa tag de uma vez. Mais granular que revalidatePath quando múltiplas páginas usam os mesmos dados.

Cache do fetch
// Cache indefinida (default em produção)
fetch(url, { cache: "force-cache" })

// Sem cache (sempre fresco)
fetch(url, { cache: "no-store" })

// Revalidar por tempo
fetch(url, { next: { revalidate: 60 } })

force-cache guarda a resposta indefinidamente. no-store busca sempre dados frescos (equivalente a SSR). revalidate faz cache com expiração temporal.

Streaming com Suspense
import { Suspense } from "react";

export default function Page() {
  return (
    <div>
      <h1>Blog</h1>
      <Suspense fallback={<p>Carregando...</p>}>
        <ListaPosts />
      </Suspense>
    </div>
  );
}

Suspense permite streaming: envia o HTML shell imediatamente e o conteúdo quando pronto. O fallback mostra enquanto carrega. Melhora TTFB e UX percebida.

Routing e Navegação


9 cards
Rota dinâmica
// app/blog/[slug]/page.tsx
export default function Post({
  params,
}: { params: { slug: string } }) {
  return <h1>{params.slug}</h1>;
}

Parênteses rectos [slug] criam segmentos dinâmicos. O valor fica em params.slug. /blog/ola-mundoparams.slug = "ola-mundo".

redirect
import { redirect } from "next/navigation";

export default async function Page() {
  const user = await getUser();
  if (!user) {
    redirect("/login");
  }
  return <p>Olá, {user.nome}</p>;
}

redirect() redirecciona no servidor (HTTP 307). Lança uma excepção internamente — não precisa de return depois. Use para proteger rotas ou redireccionar após acções.

Rewrites e redirects
// next.config.js
module.exports = {
  async rewrites() {
    return [{ source: "/api/:path*",
      destination: "https://api.externa.com/:path*" }];
  },
  async redirects() {
    return [{ source: "/velho",
      destination: "/novo", permanent: true }];
  },
};

rewrites proxy requests sem mudar o URL (transparente). redirects envia HTTP 301/308. Configurados em next.config.js. Úteis para migrações e APIs externas.

Catch-all routes
// app/docs/[...slug]/page.tsx
// /docs/a/b/c → params.slug = ["a", "b", "c"]

// Optional catch-all:
// app/docs/[[...slug]]/page.tsx
// /docs → params.slug = undefined

[...slug] captura múltiplos segmentos como array. [[...slug]] (duplos colchetes) é opcional — também corresponde à rota sem segmentos.

notFound()
import { notFound } from "next/navigation";

export default async function Post({ params }) {
  const post = await getPost(params.slug);
  if (!post) {
    notFound();
  }
  return <h1>{post.titulo}</h1>;
}

notFound() renderiza o not-found.tsx mais próximo. Lança excepção interna (como redirect). Use quando um recurso não existe na BD para retornar 404 correctamente.

useRouter
"use client";
import { useRouter } from "next/navigation";

export function Botao() {
  const router = useRouter();

  return (
    <button onClick={() => router.push("/login")}>
      Entrar
    </button>
  );
}

useRouter() permite navegação programática em Client Components. push() adiciona ao histórico. replace() substitui. back() volta. refresh() re-executa o load.

Link com prefetch
import Link from "next/link";

<Link href="/dashboard" prefetch={true}>
  Dashboard
</Link>

<Link href="/pesado" prefetch={false}>
  Página pesada
</Link>

prefetch carrega a página em background quando o link é visível. Por defeito, é automático para links estáticos. prefetch={false} desactiva para páginas pesadas ou autenticadas.

usePathname e useSearchParams
"use client";
import { usePathname, useSearchParams } from "next/navigation";

export function Filtro() {
  const pathname = usePathname();   // "/blog"
  const searchParams = useSearchParams();
  const pagina = searchParams.get("pagina"); // "2"
}

usePathname() retorna o caminho actual. useSearchParams() dá acesso aos query params. Ambos reactivos — actualizam quando a URL muda. Só em Client Components.

Route handlers dinâmicos
// app/api/posts/[id]/route.ts
export async function GET(
  request: Request,
  { params }: { params: { id: string } }
) {
  const post = await getPost(params.id);
  return Response.json(post);
}

Route handlers em pastas dinâmicas recebem params como segundo argumento. request é o objecto Request nativo. Suporta todos os verbos HTTP exportados.

API e Server Actions


10 cards
Route handler GET
// app/api/users/route.ts
export async function GET() {
  const users = await db.users.findMany();
  return Response.json(users);
}

route.ts cria um endpoint API. Exporte funções com nomes de verbos: GET, POST, PUT, DELETE. Response.json() retorna JSON com headers correctos.

Usar Server Action em form
import { criarPost } from "./actions";

export default function Form() {
  return (
    <form action={criarPost}>
      <input name="titulo" required />
      <button type="submit">Criar</button>
    </form>
  );
}

action={serverAction} liga o form directamente. Funciona sem JavaScript (progressive enhancement). O form faz POST automático e a action processa no servidor.

Streaming responses
export async function GET() {
  const stream = new ReadableStream({
    start(controller) {
      controller.enqueue("Olá ");
      setTimeout(() => {
        controller.enqueue("Mundo!");
        controller.close();
      }, 1000);
    },
  });

  return new Response(stream);
}

Route handlers suportam ReadableStream para respostas em streaming. Útil para SSE, AI responses ou dados progressivos. O cliente recebe chunks à medida que são gerados.

Route handler POST
// app/api/users/route.ts
export async function POST(request: Request) {
  const body = await request.json();
  const user = await db.users.create(body);
  return Response.json(user, { status: 201 });
}

request.json() lê o body JSON. request.formData() para forms. O segundo argumento de Response.json() define status e headers customizados.

useFormStatus e useActionState
"use client";
import { useFormStatus } from "react-dom";
import { useActionState } from "react";

function Submit() {
  const { pending } = useFormStatus();
  return <button disabled={pending}>
    {pending ? "Enviando..." : "Enviar"}
  </button>;
}

useFormStatus() dá o estado do form (pending, data). useActionState() gere o retorno da action (sucesso/erro). Ambos melhoram UX durante submissões.

CORS e middleware API
export async function OPTIONS() {
  return new Response(null, {
    headers: {
      "Access-Control-Allow-Origin": "*",
      "Access-Control-Allow-Methods": "GET, POST",
      "Access-Control-Allow-Headers": "Content-Type",
    },
  });
}

OPTIONS responde a preflight requests CORS. Defina headers Access-Control-* para permitir origens externas. Alternativa: configurar CORS no middleware.ts global.

Query params e headers
export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const pagina = searchParams.get("pagina");

  const auth = request.headers.get("Authorization");
  return Response.json({ pagina, auth });
}

new URL(request.url) dá acesso a searchParams. request.headers.get() lê headers. Ambos usam APIs web nativas (não Next.js específico).

cookies e headers
import { cookies, headers } from "next/headers";

// Ler cookie:
const token = cookies().get("token")?.value;

// Definir cookie:
cookies().set("tema", "escuro", { path: "/" });

// Ler header:
const ua = headers().get("user-agent");

cookies() e headers() acedem ao request no servidor. Funcionam em Server Components, Route Handlers e Server Actions. cookies().set() define cookies na resposta.

Server Action básica
"use server";

// app/actions.ts
export async function criarPost(formData: FormData) {
  const titulo = formData.get("titulo") as string;
  await db.posts.create({ titulo });
}

"use server" no topo marca o ficheiro como Server Actions. Funções correm apenas no servidor. Recebem FormData ou argumentos serializáveis. Substituem endpoints API para mutações.

revalidatePath em action
"use server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";

export async function criar(formData: FormData) {
  await db.posts.create({ titulo: formData.get("titulo") });
  revalidatePath("/blog");
  redirect("/blog");
}

Após mutação, revalidatePath() invalida a cache e redirect() navega. Padrão completo: guardar → invalidar → redireccionar. O utilizador vê dados frescos imediatamente.

Renderização


9 cards
Server Component (default)
// app/page.tsx (sem directiva = Server)
export default async function Page() {
  const dados = await fetch(url);
  return <div>{dados.titulo}</div>;
}

Por defeito, todos os componentes são Server Components. Podem ser async, aceder a BD e usar segredos. Zero JavaScript enviado ao cliente — renderizados no servidor.

dynamic import
import dynamic from "next/dynamic";

const Mapa = dynamic(() => import("./Mapa"), {
  ssr: false,
  loading: () => <p>Carregando mapa...</p>,
});

dynamic() faz code-splitting e lazy loading. ssr: false desactiva renderização no servidor (para libs que usam window/document). loading mostra fallback.

Hydration e erros
"use client";
import { useEffect, useState } from "react";

export function Hora() {
  const [hora, setHora] = useState("");
  useEffect(() => {
    setHora(new Date().toLocaleTimeString());
  }, []);
  return <p>{hora}</p>;
}

Hydration mismatch ocorre quando server HTML difere do client render. Valores como Date.now() ou Math.random() causam erros. Use useEffect para valores só do cliente.

Client Component
"use client";

import { useState } from "react";

export default function Contador() {
  const [n, setN] = useState(0);
  return <button onClick={() => setN(n + 1)}>{n}</button>;
}

"use client" no topo marca como Client Component. Necessário para hooks (useState, useEffect), eventos (onClick) e APIs do browser. Executa no cliente e servidor (hydration).

Suspense para streaming
import { Suspense } from "react";

export default function Page() {
  return (
    <>
      <h1>Dashboard</h1>
      <Suspense fallback={<Skeleton />}>
        <GraficoVendas />
      </Suspense>
    </>
  );
}

Suspense envia o HTML disponível imediatamente e faz stream do restante. Cada Suspense é independente — múltiplos podem resolver em paralelo. Melhora TTFB significativamente.

Compor Server + Client
// app/page.tsx (Server)
import Contador from "./Contador";

export default async function Page() {
  const dados = await fetch(url);
  return (
    <div>
      <h1>{dados.titulo}</h1>
      <Contador inicial={dados.valor} />
    </div>
  );
}

Padrão recomendado: Server busca dados e passa como props para Client Components. Mantém a lógica pesada no servidor. Client só recebe dados serializáveis e gere interactividade.

Renderização estática vs dinâmica
// Forçar estático:
export const dynamic = "force-static";

// Forçar dinâmico (SSR):
export const dynamic = "force-dynamic";

// Runtime:
export const runtime = "edge";

dynamic controla quando a página é gerada. force-static = build time. force-dynamic = cada request. runtime = "edge" executa em edge (Vercel Edge, Cloudflare Workers).

children como composição
"use client";
// Modal.tsx
export function Modal({ children }: {
  children: React.ReactNode
}) {
  const [aberto, setAberto] = useState(true);
  return aberto ? <div>{children}</div> : null;
}

// Server pode passar children:
<Modal><ServerContent /></Modal>

children permite Server Components dentro de Client Components. O conteúdo é renderizado no servidor e "projectado" no cliente. Mantém a árvore Server o mais acima possível.

Partial prerendering (PPR)
// next.config.js
experimental: { ppr: true }

// app/page.tsx
import { Suspense } from "react";

export default function Page() {
  return (
    <>
      <Header />  {/* estático */}
      <Suspense fallback={<Skeleton />}>
        <Perfil />  {/* dinâmico */}
      </Suspense>
    </>
  );
}

PPR (experimental) combina estático e dinâmico na mesma página. O shell é servido de CDN instantaneamente. Partes dinâmicas em Suspense são streamed do servidor.

Avançado e Deploy


10 cards
Middleware
// middleware.ts (raiz ou src/)
import { NextResponse } from "next/server";

export function middleware(request: Request) {
  const token = request.cookies.get("token");
  if (!token) {
    return NextResponse.redirect(new URL("/login", request.url));
  }
  return NextResponse.next();
}

export const config = {
  matcher: ["/dashboard/:path*"],
};

middleware.ts intercepta requests antes de chegar à rota. matcher limita onde corre. Use para auth, redirects, A/B testing, geolocalização. Corre no edge (rápido).

Deploy na Vercel
npm i -g vercel
vercel          # deploy preview
vercel --prod   # deploy produção

# Ou: push para GitHub
# Vercel detecta e faz deploy automático

Vercel é a plataforma nativa do Next.js. Deploy automático a cada push. Preview deployments por PR. Edge functions, ISR e image optimization incluídos. Zero configuração.

Testing (Jest + RTL)
// __tests__/page.test.tsx
import { render, screen } from "@testing-library/react";
import Home from "@/app/page";

test("mostra título", () => {
  render(<Home />);
  expect(screen.getByText("Bem-vindo")).toBeTruthy();
});

@testing-library/react renderiza componentes em testes. render() monta no DOM virtual. screen.getByText() procura elementos. Para Server Components, use jest-environment-jsdom.

Edge runtime
// app/api/geo/route.ts
export const runtime = "edge";

export async function GET(request: Request) {
  const geo = request.geo;
  return Response.json({
    pais: geo?.country,
    cidade: geo?.city,
  });
}

runtime = "edge" executa em edge locations (mais perto do utilizador). Sem acesso a Node.js APIs (fs, net). Ideal para respostas rápidas: geo, auth, redirects, A/B tests.

Docker (standalone)
# Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY . .
RUN npm ci && npm run build

FROM node:20-alpine AS runner
WORKDIR /app
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/public ./public
CMD ["node", "server.js"]

output: "standalone" gera um servidor Node mínimo. O Dockerfile multi-stage copia apenas o necessário. Imagem final ~50MB. Ideal para VPS, Kubernetes, AWS ECS.

Performance e Core Web Vitals
// app/layout.tsx
import { Inter } from "next/font/google";
const inter = Inter({ subsets: ["latin"] });

// Monitorizar:
export function reportWebVitals(metric) {
  console.log(metric.name, metric.value);
}

// next build mostra:
// Route  Size  First Load JS

Next.js optimiza automaticamente: code-splitting, image optimization, font optimization. next build mostra tamanho por rota. reportWebVitals monitoriza LCP, FID, CLS em produção.

Dynamic imports e lazy
import dynamic from "next/dynamic";

// Sem SSR (só cliente):
const Editor = dynamic(() => import("./Editor"), {
  ssr: false,
});

// Com loading:
const Grafico = dynamic(() => import("./Grafico"), {
  loading: () => <p>A carregar...</p>,
});

dynamic() divide o bundle e carrega sob demanda. ssr: false para libs que precisam de window/document. Reduz JavaScript inicial — melhora First Contentful Paint.

Static export
// next.config.js
module.exports = {
  output: "export",
};

// Build:
npm run build
// Gera pasta out/ com HTML estático

output: "export" gera um site 100% estático (sem servidor Node). HTML/CSS/JS na pasta out/. Sem SSR, API routes ou middleware. Ideal para GitHub Pages, S3, Netlify.

next.config avançado
module.exports = {
  output: "standalone",
  experimental: {
    serverActions: { bodySizeLimit: "2mb" },
  },
  headers: async () => [{
    source: "/(.*)",
    headers: [{ key: "X-Frame-Options", value: "DENY" }],
  }],
};

output: "standalone" gera build mínimo para Docker. headers adiciona headers de segurança globais. experimental activa features experimentais. Configure conforme necessidades de deploy.

Instrumentation e logging
// instrumentation.ts
export async function register() {
  if (process.env.NEXT_RUNTIME === "nodejs") {
    await import("./sentry.server.config");
  }
  if (process.env.NEXT_RUNTIME === "edge") {
    await import("./sentry.edge.config");
  }
}

instrumentation.ts corre uma vez no arranque do servidor. Ideal para inicializar Sentry, OpenTelemetry, ou conexões a BD. NEXT_RUNTIME distingue Node.js de Edge.

Estilização e Assets


9 cards
CSS Modules
// Card.module.css
.cartao { padding: 1rem; }
.titulo { font-size: 1.5rem; }

// Card.tsx
import styles from "./Card.module.css";

<div className={styles.cartao}>
  <h2 className={styles.titulo}>{titulo}</h2>
</div>

CSS Modules fazem scope automático por componente. Nomes de classe são únicos (hash). Sem conflitos globais. Ficheiros .module.css são suportados nativamente.

next/image avançado
import Image from "next/image";

<Image
  src="https://exemplo.com/foto.jpg"
  fill
  sizes="(max-width: 768px) 100vw, 50vw"
  style={{ objectFit: "cover" }}
  placeholder="blur"
  blurDataURL="data:image/..."
/>

fill para imagens responsivas (pai precisa position: relative). sizes optimiza o srcset. placeholder="blur" mostra blur-up enquanto carrega. Domínios externos devem estar em images.domains.

CSS-in-JS (styled-components)
// lib/registry.tsx
"use client";
import { ServerStyleSheet } from "styled-components";

// next.config.js:
compiler: { styledComponents: true }

// Componente:
const Titulo = styled.h1`
  color: tomato;
  font-size: 2rem;
`;

Next.js suporta styled-components com compiler.styledComponents: true. Requer registry para SSR. Alternativas: Emotion, Vanilla Extract. Tailwind é a opção mais popular.

Tailwind CSS
// globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;

// Componente:
<div className="p-4 rounded-lg shadow-md">
  <h2 className="text-xl font-bold">Título</h2>
</div>

Tailwind CSS vem como opção no create-next-app. Classes utilitárias inline. tailwind.config.ts personaliza tema. Purge automático no build — só CSS usado é incluído.

Ficheiros estáticos
public/
  logo.png
  favicon.ico
  robots.txt

// Uso:
<img src="/logo.png" />
// Servido em: https://site.com/logo.png

A pasta public/ serve ficheiros na raiz do site. Sem processamento — copiados como estão. Para imagens, prefira next/image (optimização). Ideal para robots.txt, favicons, SVGs.

Global CSS
// app/globals.css
* { box-sizing: border-box; }
body { margin: 0; font-family: sans-serif; }

// Importar no layout raiz:
// app/layout.tsx
import "./globals.css";

CSS global só pode ser importado em app/layout.tsx (raiz). Aplica-se a todas as páginas. Para estilos por componente, use CSS Modules ou Tailwind. Não importe CSS global em sub-componentes.

SVG como componente
// next.config.js
module.exports = {
  webpack(config) {
    config.module.rules.push({
      test: /\.svg$/,
      use: ["@svgr/webpack"],
    });
    return config;
  },
};

// Uso:
import Logo from "./logo.svg";
<Logo width={48} />

@svgr/webpack permite importar SVGs como componentes React. Aceita props como width, fill. Alternativa: importar como URL com next/image.

Styled JSX
export function Card() {
  return (
    <div>
      <p>Olá</p>
      <style jsx>{`
        p { color: blue; font-size: 16px; }
      `}</style>
    </div>
  );
}

styled-jsx vem incluído no Next.js. CSS dentro de <style jsx> tem scope ao componente. Suporta global para estilos não-escopados. Alternativa leve a CSS Modules.

Dark mode com Tailwind
// tailwind.config.ts
darkMode: "class",

// layout.tsx:
<html className="dark">

// Componente:
<div className="bg-white dark:bg-gray-900">
  <p className="text-black dark:text-white">Olá</p>
</div>

darkMode: "class" activa dark mode por classe no <html>. Prefixo dark: aplica estilos no modo escuro. Combine com next-themes para toggle com persistência.