Cuando un test de UI elimina un recurso y verifica la UI, solo confirma que el frontend se actualizó: la llamada a la API pudo haber fallado en silencio mientras la UI mostraba un estado de éxito optimista. Asertar el DELETE a nivel de API después lo detecta. APIRequestContext es el cliente HTTP integrado de Playwright, disponible a través del fixture request en el mismo paquete @playwright/test que ya tenés instalado. Esta guía cubre el flujo completo: GET y POST con aserciones, auth con Bearer token, sembrar datos de prueba vía API antes de que corran los tests de UI, y la diferencia entre request (cookie jar aislado) y page.request (comparte las cookies de sesión del navegador).

Qué es APIRequestContext y cuándo usarlo

APIRequestContext es el cliente HTTP integrado de Playwright. Te permite enviar requests GET, POST, PUT, PATCH, DELETE, inspeccionar respuestas, manejar headers y cookies, y asertar sobre códigos de estado y cuerpos de respuesta, todo desde dentro de un archivo .spec.ts.

No reemplaza los tests de UI. Sirve para otra cosa. Un test de UI maneja el navegador: hace clic en botones, llena formularios, espera elementos. Un test con APIRequestContext envía requests HTTP directamente al servidor, saltándose el navegador por completo. Eso lo hace más rápido, más confiable y mejor adaptado para testear la capa del backend.

¿Cuándo usas APIRequestContext en lugar de un test de UI?

  • Estás testeando validación de datos. ¿La API rechaza un campo requerido que falta?
  • Estás testeando autenticación. ¿Vuelve un 401 cuando no hay token?
  • Estás testeando lógica de negocio que vive en el servidor, no en la UI.
  • Quieres sembrar datos de prueba antes de un test de UI sin pasar por el formulario de la UI.
  • Quieres verificar un efecto secundario del backend después de una acción en la UI.

¿Cuándo te quedas con los tests de UI? Cuando estás testeando lo que el usuario realmente ve e interactúa: renderizado, navegación, comportamiento de formularios, feedback visual. Las dos capas pertenecen a una suite completa. El error es usar una donde la otra es claramente mejor.

El fixture request

Playwright expone APIRequestContext a través del fixture integrado request. Lo usas exactamente igual que page: lo declarás en la firma de la función del test y Playwright maneja la configuración.

import { test, expect } from '@playwright/test';

test('GET /api/items devuelve 200', async ({ request }) => {
  const response = await request.get('https://lab.becomeqa.com/api/items');

  expect(response.status()).toBe(200);
});

No se abre ninguna ventana del navegador. No se renderiza ningún DOM. El test runner envía un request HTTP, recibe una respuesta y corren tus aserciones. Todo termina en menos de 100 milisegundos con una conexión típica.

El fixture request crea un APIRequestContext aislado para cada test. Tiene su propio cookie jar, sus propios headers y ninguna conexión con ningún contexto del navegador. Ese aislamiento es intencional: tus tests de API son independientes de lo que esté haciendo el navegador.

Hacer requests GET y asertar respuestas

Un test GET tiene tres partes: enviar el request, verificar el estado, verificar el cuerpo.

import { test, expect } from '@playwright/test';

test('GET /api/items devuelve una lista válida', async ({ request }) => {
  const response = await request.get('https://lab.becomeqa.com/api/items');

  // Verificación de estado
  expect(response.status()).toBe(200);
  expect(response.ok()).toBeTruthy(); // shorthand: true para cualquier 2xx

  // Parsear el cuerpo JSON
  const items = await response.json();

  // Verificaciones de forma
  expect(Array.isArray(items)).toBe(true);
  expect(items.length).toBeGreaterThan(0);

  // Verificar las propiedades de un ítem
  const primero = items[0];
  expect(primero).toHaveProperty('id');
  expect(primero).toHaveProperty('destination');
  expect(primero).toHaveProperty('status');
});

response.ok() devuelve true para cualquier código de estado en el rango 200-299. Úsalo cuando solo necesitas confirmar el éxito sin importar el código exacto. Usa response.status() cuando el código específico importa: 200 vs 201 vs 204 tienen significados diferentes.

También puedes leer el cuerpo como texto o como buffer:

const texto = await response.text();
const buffer = await response.body(); // Buffer

Para asertar sobre headers de respuesta:

test('la respuesta incluye content-type JSON', async ({ request }) => {
  const response = await request.get('https://lab.becomeqa.com/api/items');

  const headers = response.headers();
  expect(headers['content-type']).toContain('application/json');
});

Requests POST con cuerpo JSON

Los requests POST envían datos al servidor. Pasá el payload vía la opción data y Playwright lo serializa automáticamente a JSON y establece Content-Type: application/json.

test('POST /api/items crea un recurso', async ({ request }) => {
  const response = await request.post('https://lab.becomeqa.com/api/items', {
    data: {
      destination: 'Kioto',
      status: 'planned',
      notes: 'Visitar el bosque de bambú de Arashiyama'
    }
  });

  // Una API bien diseñada devuelve 201 Created para recursos nuevos
  expect(response.status()).toBe(201);

  const creado = await response.json();
  expect(creado).toHaveProperty('id');
  expect(creado.destination).toBe('Kioto');
  expect(creado.status).toBe('planned');
});

Guardá el id devuelto cuando necesitás limpiar o encadenar requests:

test('crear y luego eliminar un ítem', async ({ request }) => {
  const createRes = await request.post('https://lab.becomeqa.com/api/items', {
    data: { destination: 'Tbilisi', status: 'planned' }
  });
  expect(createRes.status()).toBe(201);

  const { id } = await createRes.json();

  const deleteRes = await request.delete(`https://lab.becomeqa.com/api/items/${id}`);
  expect(deleteRes.status()).toBe(204);

  // Confirmar que ya no existe
  const getRes = await request.get(`https://lab.becomeqa.com/api/items/${id}`);
  expect(getRes.status()).toBe(404);
});

Para datos form-encoded, reemplazá data por form:

const response = await request.post('https://lab.becomeqa.com/api/login', {
  form: {
    username: 'admin@becomeqa.com',
    password: 'testpass123'
  }
});

Autenticación: Bearer tokens y headers personalizados

La mayoría de las APIs reales requieren autenticación. Los dos patrones más comunes son los Bearer tokens y las API keys pasadas en headers.

Bearer token: primero haces login, luego usas el token

test('request autenticado con Bearer token', async ({ request }) => {
  // Paso 1: obtener un token
  const loginRes = await request.post('https://lab.becomeqa.com/api/auth/login', {
    data: {
      username: 'admin@becomeqa.com',
      password: 'testpass123'
    }
  });
  expect(loginRes.status()).toBe(200);

  const { token } = await loginRes.json();

  // Paso 2: usarlo en los requests siguientes
  const itemsRes = await request.get('https://lab.becomeqa.com/api/items', {
    headers: {
      Authorization: `Bearer ${token}`
    }
  });

  expect(itemsRes.status()).toBe(200);
});

Cuando el mismo token se usa en muchos tests, mueve el paso de login a un bloque beforeAll y compartí el token en toda la suite:

import { test, expect } from '@playwright/test';

let authToken: string;

test.beforeAll(async ({ request }) => {
  const res = await request.post('https://lab.becomeqa.com/api/auth/login', {
    data: {
      username: 'admin@becomeqa.com',
      password: 'testpass123'
    }
  });
  const body = await res.json();
  authToken = body.token;
});

test('GET ítems como usuario autenticado', async ({ request }) => {
  const response = await request.get('https://lab.becomeqa.com/api/items', {
    headers: { Authorization: `Bearer ${authToken}` }
  });
  expect(response.status()).toBe(200);
});

API key vía un header común: configúrala una vez en playwright.config.ts

// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  use: {
    baseURL: 'https://lab.becomeqa.com',
    extraHTTPHeaders: {
      'X-Api-Key': process.env.API_KEY ?? ''
    }
  }
});

Cada request que envíe el fixture request incluirá ese header automáticamente. No hace falta repetirlo en cada test.

Guardá los secretos en variables de entorno, nunca hardcodeados. Usá un archivo .env localmente (con dotenv) y secretos de GitHub Actions en CI. El acceso a process.env de Playwright funciona igual en los dos entornos.

playwright.request.newContext() para tests de API independientes

El fixture request es conveniente dentro de los tests, pero a veces necesitás un APIRequestContext fuera del test runner: en un archivo de global setup, en un script de utilidad, o cuando querés un contexto con su propia configuración separada de la por defecto.

playwright.request.newContext() crea un contexto independiente que controlás explícitamente:

// global-setup.ts
import { chromium, request } from '@playwright/test';

async function globalSetup() {
  // Crear un contexto de API independiente
  const apiContext = await request.newContext({
    baseURL: 'https://lab.becomeqa.com',
    extraHTTPHeaders: {
      'Content-Type': 'application/json'
    }
  });

  // Sembrar datos de prueba antes de que corra cualquier test
  await apiContext.post('/api/items', {
    data: { destination: 'Lisboa', status: 'planned' }
  });

  // Siempre eliminá el contexto cuando terminés
  await apiContext.dispose();
}

export default globalSetup;

Referencias el archivo de global setup en tu config:

// playwright.config.ts
export default defineConfig({
  globalSetup: './global-setup.ts',
  use: {
    baseURL: 'https://lab.becomeqa.com'
  }
});

newContext() acepta las mismas opciones que el bloque de config use: baseURL, extraHTTPHeaders, httpCredentials, ignoreHTTPSErrors. Llamá dispose() cuando el contexto ya no sea necesario. Cierra las conexiones abiertas y limpia las cookies.
playwright.request.newContext() y el fixture request crean instancias de APIRequestContext. La diferencia está en el ciclo de vida: el fixture se crea y destruye automáticamente por test. newContext() te da control manual, útil para global setup, scripts de teardown o contextos que abarcan múltiples tests.

Combinar setup via API con verificación en la UI

Acá es donde APIRequestContext da su mayor retorno. La parte más lenta y frágil de un test de UI suele ser el setup: llenar formularios, esperar estado, navegar por pantallas solo para llegar al escenario que querés testear. Reemplazá eso con una llamada a la API.

La API crea los datos, la UI verifica que se renderizan

test('un ítem creado via API aparece en la lista de la UI', async ({ page, request }) => {
  // Setup rápido y confiable. Sin navegador involucrado.
  const createRes = await request.post('https://lab.becomeqa.com/api/items', {
    data: { destination: 'Porto', status: 'planned' }
  });
  expect(createRes.status()).toBe(201);
  const { id } = await createRes.json();

  // Ahora testéa lo que importa: ¿la UI lo muestra correctamente?
  await page.goto('https://lab.becomeqa.com/items');
  await expect(page.getByText('Porto')).toBeVisible();
  await expect(page.getByTestId(`item-${id}`)).toBeVisible();

  // Limpieza vía API. También más rápida que hacer clic en un flujo de eliminación en la UI.
  await request.delete(`https://lab.becomeqa.com/api/items/${id}`);
});

La UI ejecuta la acción, la API confirma el efecto secundario

test('eliminar un ítem via UI lo elimina de la base de datos', async ({ page, request }) => {
  // Crear vía API
  const createRes = await request.post('https://lab.becomeqa.com/api/items', {
    data: { destination: 'Valletta', status: 'planned' }
  });
  const { id } = await createRes.json();

  // Eliminar a través de la UI. Esto es lo que realmente estamos testeando.
  await page.goto('https://lab.becomeqa.com/items');
  await page.getByTestId(`item-${id}`).getByRole('button', { name: 'Delete' }).click();
  await page.getByRole('button', { name: 'Confirm' }).click();

  // Verificar el estado del backend, no solo el estado de la UI
  const checkRes = await request.get(`https://lab.becomeqa.com/api/items/${id}`);
  expect(checkRes.status()).toBe(404);
});

El segundo patrón está subutilizado. Cuando un usuario elimina algo, la UI puede actualizarse de forma optimista y verse correcta incluso si la llamada real a la API falló. Asertar a nivel de API captura ese fallo.

Helpers de API reutilizables y fixtures

Repetir request.post('/api/auth/login', ...) en cada archivo de test es ruido. Construye una pequeña clase helper y expónela a través de un fixture personalizado.

Primero, la clase helper:

// lib/api-client.ts
import { APIRequestContext } from '@playwright/test';

export class ApiClient {
  constructor(private request: APIRequestContext) {}

  async login(username: string, password: string): Promise<string> {
    const res = await this.request.post('/api/auth/login', {
      data: { username, password }
    });
    const { token } = await res.json();
    return token;
  }

  async createItem(data: { destination: string; status: string; notes?: string }) {
    const res = await this.request.post('/api/items', { data });
    expect(res.status()).toBe(201);
    return res.json();
  }

  async deleteItem(id: string) {
    await this.request.delete(`/api/items/${id}`);
  }

  async getItem(id: string) {
    return this.request.get(`/api/items/${id}`);
  }
}

Luego expónela a través de un fixture personalizado:

// fixtures.ts
import { test as base } from '@playwright/test';
import { ApiClient } from './lib/api-client';

type Fixtures = {
  api: ApiClient;
};

export const test = base.extend<Fixtures>({
  api: async ({ request }, use) => {
    const client = new ApiClient(request);
    await use(client);
  }
});

export { expect } from '@playwright/test';

Los tests se vuelven mucho más legibles:

import { test, expect } from './fixtures';

test('crear y verificar ítem', async ({ api, page }) => {
  const item = await api.createItem({ destination: 'Riga', status: 'planned' });

  await page.goto('https://lab.becomeqa.com/items');
  await expect(page.getByText('Riga')).toBeVisible();

  await api.deleteItem(item.id);
});

El helper encapsula la lógica de requests. El fixture gestiona el ciclo de vida. El test se enfoca en el escenario. Cada capa tiene un solo trabajo.

Mantené las llamadas expect fuera de la clase ApiClient, excepto para precondiciones obligatorias como verificar un estado 201 después de un create. Las aserciones en los helpers hacen que los fallos sean más difíciles de rastrear porque el stack apunta al helper, no al test.

request vs page.request: cuál es la diferencia

Los dos son instancias de APIRequestContext. La distinción está en cómo manejan las cookies y el estado de sesión.

request, el fixture, es un contexto aislado. Tiene su propio cookie jar, separado de cualquier navegador. No comparte estado con page. Cuando inicias sesión vía request, el navegador no lo sabe. page.request está vinculado al contexto del navegador al que pertenece page. Comparte cookies con la página. Si el usuario inicia sesión a través del navegador, page.request lleva esas cookies. Si page.request establece una cookie, el navegador la ve.

test('diferencia entre request y page.request', async ({ page, request }) => {
  // Iniciar sesión a través del navegador
  await page.goto('https://lab.becomeqa.com/login');
  await page.getByLabel('Username').fill('admin@becomeqa.com');
  await page.getByLabel('Password').fill('testpass123');
  await page.getByRole('button', { name: 'Login' }).click();

  // page.request lleva la cookie de auth, devuelve 200
  const conCookies = await page.request.get('https://lab.becomeqa.com/api/items');
  expect(conCookies.status()).toBe(200);

  // el fixture request no tiene cookie, devuelve 401
  const sinCookies = await request.get('https://lab.becomeqa.com/api/items');
  expect(sinCookies.status()).toBe(401);
});

¿Cuál usar? Si tu test involucra tanto una sesión del navegador como llamadas a la API que deben usar la misma auth, usá page.request. Si estás escribiendo tests de API puros sin navegador, usá el fixture request. Si necesitás un contexto completamente independiente con headers o base URL personalizados, usá playwright.request.newContext().

FAQ

¿Todavía necesito Postman?

Postman es una buena herramienta de exploración. Cuando encuentras una API por primera vez y no conoces su forma, abres Postman, exploras, lees las respuestas, entiendes qué necesitas. Una vez que sabés qué estás testeando, escribilo en Playwright. Obtenés control de versiones, integración con CI y la posibilidad de combinar aserciones de API y UI en el mismo test, ninguna de las cuales te da Postman.

¿Puedo usar APIRequestContext para testear GraphQL?

Sí. GraphQL sobre HTTP es un request POST a un único endpoint con un cuerpo JSON que contiene query y opcionalmente variables. La opción data lo maneja directamente:

const response = await request.post('https://lab.becomeqa.com/graphql', {
  data: {
    query: `
      query GetItem($id: ID!) {
        item(id: $id) {
          id
          destination
          status
        }
      }
    `,
    variables: { id: '123' }
  }
});

const { data } = await response.json();
expect(data.item.destination).toBe('Kioto');

¿APIRequestContext sigue los redirects automáticamente?

Sí, por defecto sigue hasta 20 redirects. Para deshabilitar el seguimiento de redirects e inspeccionar directamente la respuesta de redirect, pasá maxRedirects: 0 en las opciones del request.

¿Cómo manejo tests de API que son lentos o tienen rate limiting?

Establecé un timeout personalizado en las opciones del request o aumentá timeout en tu playwright.config.ts para el proyecto de API. Para APIs con rate limiting en testing, considerá sembrar los datos en globalSetup una sola vez en lugar de crearlos frescos en cada test.

¿Cuál es la diferencia entre response.json() y response.text()? response.json() parsea el cuerpo y devuelve un objeto JavaScript. Lanza un error si el cuerpo no es JSON válido. response.text() devuelve el string crudo. Usá text() para debugging o cuando el endpoint devuelve un formato no JSON como texto plano o XML. → See also: Fixtures de Playwright Explicados: De los Integrados a los Personalizados | Configuración y Limpieza Global en Playwright | API Testing con Playwright: Más Allá de la UI | Testing de API Avanzado con Playwright: Patrones para Proyectos Reales | Autenticación en Tests de API: Claves API, Tokens Bearer, OAuth2, JWT