Playwright API Testing with APIRequestContext (TypeScript)

Playwright API Testing with APIRequestContext (TypeScript)

Playwright is famous for browser automation, but it’s also one of the best API testing frameworks for TypeScript: fast parallel runs, rich assertions, fixtures, retries, HTML reports and trace files, all without opening a browser. Everything starts with APIRequestContext. In this lesson you’ll set up a Playwright API project from scratch, learn every request option you’ll actually use, and avoid the one baseURL mistake that silently breaks almost every beginner’s first suite.

Quick answerAPIRequestContext is Playwright’s HTTP client. Use the built-in request fixture in tests (request.get(), post(), put(), patch(), delete(), head(), fetch()), or create your own with request.newContext({ baseURL, extraHTTPHeaders, httpCredentials }) and dispose() it afterwards. Each call returns an APIResponse with status(), ok(), headers(), json(), text() and body(). Set baseURL with a trailing slash and write request paths without a leading slash.
Advertisement

Why Playwright for API Testing?

  • One framework for UI and API: create data through the API, verify it in the UI, share fixtures and reports.
  • TypeScript first: autocompletion, type-safe clients and schemas.
  • Fast: tests run in parallel workers by default; API tests take milliseconds.
  • Batteries included: retries, tags, projects, HTML/JUnit reports, traces, CI examples.

Already know the Playwright basics? The Playwright with TypeScript tutorial covers installation and the test runner in depth; the shorter Playwright API testing intro is a good warm-up. This module goes deeper and builds a full framework step by step.

Project Setup (5 Minutes)

mkdir sqa-api-tests && cd sqa-api-tests
npm init -y
npm install -D @playwright/test typescript @types/node
# Browsers are NOT needed for pure API tests, so you can skip "npx playwright install"

Create playwright.config.ts:

import { defineConfig } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  reporter: [['list'], ['html', { open: 'never' }]],
  use: {
    // Trailing slash matters: request paths are resolved relative to this URL.
    baseURL: process.env.API_BASE_URL ?? 'https://sqatools.in/wp-json/sqa-api/v1/',
    extraHTTPHeaders: { Accept: 'application/json' },
  },
});

Every request in every test now starts from the practice API’s base URL and sends Accept: application/json. Switching to another environment is one environment variable: API_BASE_URL=https://qa.example.com/api/ npx playwright test.

Advertisement

The baseURL Trap (Read This Twice)

Playwright resolves request paths the same way a browser resolves links. We ran all three combinations against the live API:

baseURLPath in testActual URL calledResult
.../sqa-api/v1/'users/1'https://sqatools.in/wp-json/sqa-api/v1/users/1200 ✔
.../sqa-api/v1/'/users/1'https://sqatools.in/users/1404 ✘
.../sqa-api/v1 (no slash)'users/1'https://sqatools.in/wp-json/sqa-api/users/1404 ✘
Rule If your API has a path prefix (/api/v1, /wp-json/...), end baseURL with / and write paths without a leading /. A leading slash means “from the domain root” and drops the prefix. If your tests mysteriously get 404s, print res.url() first.

Your First API Tests

tests/request-basics.spec.ts:

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

test.describe('APIRequestContext basics', () => {
  test('GET with the built-in request fixture', async ({ request }) => {
    const res = await request.get('users/1');            // resolved against baseURL

    expect(res.status()).toBe(200);
    expect(res.ok()).toBeTruthy();
    expect(res.url()).toBe('https://sqatools.in/wp-json/sqa-api/v1/users/1');
    expect(res.headers()['content-type']).toContain('application/json');

    const body = await res.json();
    expect(body.data.name).toBe('Asha Verma');
  });

  test('query parameters with params', async ({ request }) => {
    const res = await request.get('users', { params: { role: 'admin', sort: 'age', order: 'desc' } });
    await expect(res).toBeOK();
    const { data } = await res.json();
    expect(data.map((u: { id: number }) => u.id)).toEqual([7, 1]);
  });

  test('POST JSON, form and raw bodies', async ({ request }) => {
    const json = await request.post('objects', { data: { name: 'json body', ttl: 30 } });
    expect(json.status()).toBe(201);

    const form = await request.post('echo', { form: { name: 'Asha', city: 'Pune' } });
    expect((await form.json()).headers['content-type']).toContain('application/x-www-form-urlencoded');

    const raw = await request.post('validate/user', {
      headers: { 'Content-Type': 'application/json' },
      data: '{"name": "Asha", ',                          // a string is sent as-is
    });
    expect(raw.status()).toBe(400);
  });

  test('a separate context with its own settings', async () => {
    const api = await request.newContext({
      baseURL: 'https://sqatools.in/wp-json/sqa-api/v1/',
      extraHTTPHeaders: { 'X-API-Key': 'sqa-key-12345' },
      timeout: 10_000,
    });
    const res = await api.get('apikey/profile');
    expect(res.status()).toBe(200);
    await api.dispose();                                  // always free the context
  });

  test('redirects, timeouts and failOnStatusCode', async ({ request }) => {
    const noFollow = await request.get('redirect', { maxRedirects: 0 });
    expect(noFollow.status()).toBe(302);
    expect(noFollow.headers()['location']).toBeTruthy();

    await expect(request.get('delay/5', { timeout: 2_000 })).rejects.toThrow(/Timeout/);
    await expect(request.get('users/999', { failOnStatusCode: true })).rejects.toThrow(/404/);
  });
});
npx playwright test tests/request-basics.spec.ts
npx playwright show-report        # open the HTML report

All five tests pass against the live practice API in a few seconds.

Advertisement

Request Methods and Options

MethodUse
request.get(url, options)GET
request.post / put / patch / deleteWrite methods
request.head(url)Headers only
request.fetch(url, { method: 'OPTIONS' })Any method (OPTIONS, custom)
OptionPurposeExample
paramsQuery string (encoded for you){ page: 2, per_page: 5 }
headersPer-request headers{ 'If-Match': '"v1"' }
dataBody: object → JSON (sets Content-Type), string/Buffer → sent as-is{ name: 'A' }
formURL-encoded form body{ user: 'a', pass: 'b' }
multipartFile uploads{ file: fs.createReadStream('a.pdf') }
timeoutPer-request timeout in ms (0 = none)2_000
maxRedirects0 to assert the redirect itself0
failOnStatusCodeThrow on non-2xx/3xxHandy for setup steps
maxRetriesRetry on network errors (not on HTTP errors)2
ignoreHTTPSErrorsSelf-signed test certificatesNever for production

The APIResponse Object

MethodReturns
status() / statusText()200 / “OK”
ok()true for 200 to 299
headers()Object with lower-case header names
headersArray()Array of {name, value}, needed for repeated headers like Set-Cookie
json() / text() / body()Parsed JSON / string / Buffer
url()Final URL (after redirects)
Playwright doesn’t throw on 4xx/5xx A 404 is a normal response you can assert on, which is exactly what negative tests need. Use await expect(res).toBeOK() when you require success: on failure it prints the status and response body, making failures easy to read.
Advertisement

request Fixture vs newContext vs page.request

Use when
request fixtureMost tests; uses config use settings (baseURL, headers); created and disposed for you
request.newContext() (imported) or playwright.request.newContext() (fixture)Different settings: another base URL, auth headers per role, separate cookies; call dispose()
page.request / context.requestHybrid UI + API tests: shares cookies with the browser context, so an API login logs the page in too
Real-life example: fast UI test setup Instead of clicking through 6 screens to create a test order, a UI test calls page.request.post('/api/orders', ...) in two lines, then opens the order page and checks the UI. The test runs 10x faster and fails only for real UI problems. You’ll build this in Project 5.

Common Beginner Mistakes

  • Leading slash in paths with a prefixed baseURL (404s).
  • Forgetting await before request.get() or res.json().
  • Reading the body twice from different methods is fine, but don’t forget await on each.
  • Not disposing contexts you created (memory leaks in long runs).
  • Using failOnStatusCode: true globally, then wondering why negative tests throw.
  • Hardcoding URLs in tests instead of baseURL.
Advertisement

Interview Questions: Playwright APIRequestContext

How do you do API testing in Playwright?

Use the request fixture (an APIRequestContext) inside tests to send HTTP requests with get, post, put, patch, delete or fetch, then assert on the APIResponse: status, headers and parsed JSON. Configure baseURL and shared headers in playwright.config.ts.

What is APIRequestContext?

Playwright’s API for sending HTTP requests from Node.js. It manages cookies, base URL, default headers, credentials and timeouts. It’s available as the request fixture, via request.newContext(), and as page.request bound to a browser context.

Does Playwright need a browser for API tests?

No. APIRequestContext sends requests directly from Node.js, so pure API tests don’t launch browsers and don’t need browser binaries installed.

How do you send query parameters and JSON bodies?

Pass params for the query string and data for the body. When data is an object, Playwright serialises it as JSON and sets Content-Type: application/json; strings and Buffers are sent as-is. Use form for URL-encoded and multipart for files.

Why does a request with a leading slash return 404 when baseURL has a path?

Paths are resolved like browser URLs: a leading slash means the domain root, so the base path is dropped. End baseURL with a slash and use relative paths without a leading slash.

Advertisement
Does Playwright throw an error for 4xx or 5xx responses?

Not by default; the response is returned so you can assert on it. Set failOnStatusCode: true to throw for non-2xx/3xx, or use expect(response).toBeOK().

What is the difference between request and page.request?

The request fixture is an isolated API context. page.request belongs to the page’s browser context and shares its cookie storage, so API calls and the UI share login state.

How do you test a redirect without following it?

Pass maxRedirects: 0 and assert the 3xx status and the Location header.

How do you set headers for all requests?

Use extraHTTPHeaders in the config’s use section or in request.newContext(); add per-request headers with the headers option.

Why should you dispose an APIRequestContext?

Contexts you create with newContext hold resources such as connections and stored responses; dispose() releases them. Fixture contexts are disposed automatically.

Advertisement
View the full API testing syllabus