1

Testes Automatizados para APIs Next.js: Vitest, MSW e Playwright

O problema real: APIs Next.js sem cobertura de teste

Route Handlers do App Router vivem em arquivos route.ts que exportam funções HTTP (GET, POST, PUT, DELETE). Server Actions vivem em arquivos "use server". As duas abstrações parecem simples até você precisar testar: elas dependem de objetos Request/NextRequest, acessam banco, chamam APIs externas e rodam em contexto de servidor Node.js.

O resultado comum: zero testes automatizados, ou testes frágeis que mockam tudo e não pegam nenhum bug real.

A stack que resolve isso sem inventar framework próprio: Vitest para testes unitários e de integração, MSW para interceptar chamadas HTTP a serviços externos, e Playwright para E2E que bate na API real rodando. Cada ferramenta cobre uma camada específica, sem sobreposição.

Onde cada ferramenta entra

CritérioVitestMSWPlaywright
CamadaUnitário e integraçãoInterceptação de redeE2E (browser ou API)
VelocidadeMilissegundos por testeNão roda sozinho (acopla ao runner)Segundos por teste
Precisa do servidor Next.js rodando?NãoNãoSim
Testa lógica de negócio isolada?SimIndiretamente (isola dependências)Não (testa o sistema inteiro)
Testa contrato HTTP real?Parcialmente (você monta o Request)Sim (intercepta no nível de rede)Sim
Setup de CITrivialTrivialPrecisa de browser headless

A regra prática: Vitest cobre 70-80% dos testes. MSW entra quando a Route Handler chama API externa (Stripe, OpenAI, qualquer terceiro). Playwright cobre os fluxos críticos de ponta a ponta.

Configurando Vitest para o App Router

O Vitest precisa resolver os aliases do Next.js (@/) e entender o ambiente Node.js. A configuração mínima funcional:

// vitest.config.ts
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
import path from "node:path";

export default defineConfig({
  plugins: [react()],
  test: {
    environment: "node", // Route Handlers rodam em Node, não em jsdom
    globals: true,
    include: ["**/*.test.ts", "**/*.test.tsx"],
    setupFiles: ["./tests/setup.ts"],
  },
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./src"),
    },
  },
});

O arquivo de setup registra o MSW e limpa estado entre testes:

// tests/setup.ts
import { afterAll, afterEach, beforeAll } from "vitest";
import { server } from "./mocks/server";

beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
// "error" em vez de "warn" força você a declarar todo request externo
// Se um teste faz fetch para URL não mockada, falha imediatamente
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

Testando Route Handlers com Vitest

Route Handlers são funções que recebem Request (ou NextRequest) e retornam Response (ou NextResponse). Isso significa que você pode chamá-las diretamente, sem subir o servidor:

// src/app/api/products/route.ts
import { NextRequest, NextResponse } from "next/server";
import { db } from "@/lib/database";

export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url);
  const category = searchParams.get("category");

  const products = await db.product.findMany({
    where: category ? { category } : undefined,
    take: 50,
  });

  return NextResponse.json(products);
}

export async function POST(request: NextRequest) {
  const body = await request.json();

  if (!body.name || typeof body.price !== "number") {
    return NextResponse.json(
      { error: "name (string) e price (number) são obrigatórios" },
      { status: 400 }
    );
  }

  const product = await db.product.create({
    data: { name: body.name, price: body.price, category: body.category },
  });

  return NextResponse.json(product, { status: 201 });
}

O teste unitário chama a função diretamente, injetando um NextRequest construído na mão:

// src/app/api/products/route.test.ts
impor

---

Leia o artigo completo em [https://www.vivodecodigo.com.br/nextjs/testes-automatizados-api-nextjs-vitest-msw-playwright](https://www.vivodecodigo.com.br/nextjs/testes-automatizados-api-nextjs-vitest-msw-playwright)
Carregando publicação patrocinada...