The Playwright request fixture is a full HTTP client living inside @playwright/test.
This lecture walks through the lifecycle, sets a baseURL, authenticates against
restful-booker, walks the four CRUD verbs, contrasts PUT and PATCH, and finishes with
storage state reuse and retry / backoff patterns.
01The request fixture@playwright/test
request is one of the built-in fixtures Playwright injects into every test.
You destructure it from the test args and call it like a small fetch wrapper.
Unlike browser navigation, there is no page, no rendering, no JavaScript execution -
just HTTP.
tests/intro.api.spec.ts
import { test, expect } from'@playwright/test';
test('request fixture is a thin HTTP client', async ({ request }) => {
const res = await request.get('https://restful-booker.herokuapp.com/ping');
expect(res.ok()).toBeTruthy();
expect(res.status()).toBe(201);
const text = await res.text();
expect(text).toContain('Created');
});
The eight methods on APIRequestContext:
get, post, put, patch,
delete, head, fetch (verb agnostic), and
dispose (used when you manage contexts yourself).
flowchart LR
T[test()] --> RFX[request fixture]
RFX --> CTX[APIRequestContext]
CTX --> M{verb}
M --> G[get]
M --> P[post]
M --> PU[put]
M --> PA[patch]
M --> D[delete]
M --> H[head]
M --> F[fetch]
G & P & PU & PA & D & H & F --> NET[HTTP socket]
NET --> API[restful-booker]
One fixture, eight verbs, one shared HTTP stack
02Page-attached vs newContextrequest.newContext
The built-in request fixture is "page-attached" - it shares cookies with
the browser context that the same test uses. That's useful when a test signs into the
UI and then calls an API as the same user.
When you want full control - custom headers, isolated cookies, a different baseURL -
you call request.newContext() and dispose of it at the end.
flowchart TB
C[playwright.config.ts] --> B[baseURL = restful-booker.herokuapp.com]
T[test calls request.get '/booking/1'] --> R{relative or absolute?}
R -->|relative| J[join with baseURL]
R -->|absolute| A[use as-is]
J --> URL[final: https://restful-booker.herokuapp.com/booking/1]
A --> URL
baseURL resolution - the same algorithm as the browser uses for HTML links
Env override. Use BASE_URL=https://staging.example.com npx playwright test to point the same specs at staging. Never hardcode environments in spec files.
04The endpointsrestful-booker
restful-booker is a public sandbox by Mark Winteringham. It mimics a hotel booking API
with eight endpoints. Documentation lives at restful-booker.herokuapp.com/apidoc.
The eight verbs you will use throughout this lecture
sequenceDiagram
participant C as client (request fixture)
participant S as restful-booker
C->>S: GET /ping
S-->>C: 201 Created
C->>S: POST /auth { username:'admin', password:'password123' }
S-->>C: 200 { token: 'abc' }
C->>S: POST /booking { ... }
S-->>C: 200 { bookingid, booking }
C->>S: GET /booking/{id}
S-->>C: 200 { firstname, lastname, ... }
C->>S: PUT /booking/{id} Authorization: Bearer or Cookie: token=
S-->>C: 200 { ... }
C->>S: DELETE /booking/{id} Cookie: token=
S-->>C: 201 Created
C->>S: GET /booking/{id}
S-->>C: 404 Not Found
The full lifecycle of a single booking
05Auth - POST /auth and Bearer attachtoken
restful-booker uses a custom token scheme. Send the literal credentials
admin / password123 as JSON to POST /auth.
The server returns { token: 'abc123' }. From then on, mutating endpoints
(PUT, PATCH, DELETE) require either a Cookie: token=abc123 header or
Authorization: Basic ... with the admin password.
Now every test that imports { test } from this file gets an
authed request context, ready to mutate bookings without a manual auth call.
sequenceDiagram
participant SPEC as spec.ts
participant FX as auth fixture
participant API as restful-booker
SPEC->>FX: needs { authed }
FX->>API: POST /auth { admin, password123 }
API-->>FX: 200 { token }
FX->>FX: build newContext with Cookie header
FX-->>SPEC: authed (APIRequestContext)
SPEC->>API: PUT /booking/42 (authed)
API-->>SPEC: 200 { booking }
SPEC->>FX: test ends -> dispose
FX-->>API: socket closed
The auth fixture runs once per test, exposes a ready-to-use contextThe fixture is the only place the cookie lives - tests stay clean
06POST /booking - the full payloadcreate
Creating a booking takes a JSON object with seven fields. Two of them are date strings
wrapped in a nested bookingdates object. The server returns the new id and
echoes the booking back.
Notice POST /booking does not need a token. Anyone can create a booking. The
token only matters for mutating an existing booking. This is intentional in the sandbox
and is a useful talking point about API design.
07GET /booking/{id} and assertionsread
Reading a booking is the smallest meaningful test - GET, status 200, body has the
right shape. This is also where you typically run a schema match (lecture 2 covers
AJV).
tests/booking-read.api.spec.ts
import { test, expect } from'./fixtures/auth';
test('read a booking by id', async ({ request }) => {
// list ids firstconst list = await request.get('/booking');
expect(list.ok()).toBeTruthy();
const ids = (await list.json()) as { bookingid: number }[];
expect(ids.length).toBeGreaterThan(0);
const id = ids[0].bookingid;
const one = await request.get(`/booking/${id}`);
expect(one.status()).toBe(200);
const body = await one.json();
expect(body).toMatchObject({
firstname: expect.any(String),
lastname: expect.any(String),
totalprice: expect.any(Number),
depositpaid: expect.any(Boolean),
bookingdates: {
checkin: expect.stringMatching(/^\d{4}-\d{2}-\d{2}$/),
checkout: expect.stringMatching(/^\d{4}-\d{2}-\d{2}$/),
},
});
});
toMatchObject vs full equality. Use toMatchObject for partial assertions - it tolerates new optional fields the API adds later. Use deep equal (toEqual) only on contract tests you own end-to-end.
08PUT vs PATCH semanticsupdate
PUT replaces the resource. PATCH updates fields you send. Both work on
/booking/{id} in restful-booker, but the contract is different.
stateDiagram-v2
[*] --> Created : POST /booking
Created --> Updated : PUT or PATCH /booking/id
Updated --> Updated : PUT or PATCH again
Created --> Deleted : DELETE /booking/id
Updated --> Deleted : DELETE /booking/id
Deleted --> [*] : GET returns 404
The booking resource state machine
10storageState - reuse the token across testsauth setup
POSTing to /auth once per test wastes time when you have 30 tests.
Playwright's globalSetup + storageState pattern lets you
authenticate once and feed the saved state to every spec.
flowchart LR
GS[global-setup.ts] --> A[POST /auth]
A --> J[.auth/state.json]
J --> T1[test 1]
J --> T2[test 2]
J --> T3[test N]
T1 --> API[restful-booker]
T2 --> API
T3 --> API
One auth call, N tests reuse the saved cookie
Token TTL. restful-booker tokens are short-lived. For long suites, regenerate state inside globalSetup on every CI run. Don't cache it in your repo.
11Retry and backoffresilience
Public sandboxes occasionally hiccup. Wrap fragile calls in a tiny retry helper rather
than relying on Playwright's outer retries (which retries the whole test).
sequenceDiagram
participant T as test
participant R as request
participant A as authed
participant API as restful-booker
T->>R: POST /booking
R->>API: ...
API-->>R: 200 { bookingid }
R-->>T: bookingid
T->>R: GET /booking/id
R->>API: ...
API-->>R: 200
R-->>T: body
T->>A: PUT /booking/id
A->>API: + Cookie token
API-->>A: 200
A-->>T: updated
T->>A: DELETE /booking/id
A->>API: + Cookie token
API-->>A: 201
T->>R: GET /booking/id
R->>API: ...
API-->>R: 404
R-->>T: gone
The complete chain - one test, five trips, full coverage
DDrills - eight CRUD exercises
All targets use https://restful-booker.herokuapp.com. Default admin: admin / password123.
1Ping smoke
Write a single-line test that GETs /ping and asserts status 201. Use the baseURL in your config, not the absolute URL.
Hint
Set baseURL in playwright.config.ts, then call request.get('/ping').
2Token in fixture
Build the auth.ts fixture shown in section 5. Print the token once with console.log, then make sure the print is gone before you commit.
Hint
base.extend<{ token: string }> then expose it through use(token).
3POST a booking with your own name
Create a booking with your real firstname / lastname, totalprice 500, checkin / checkout one week apart. Assert the response contains bookingid as a number greater than zero.
Hint
expect(body.bookingid).toBeGreaterThan(0);
4List the first ten ids
GET /booking, slice the first ten ids, and GET each one in sequence. Assert all responses are 200.
Hint
Loop with for (const { bookingid } of list.slice(0, 10)) and await each call.
5PATCH only one field
Create, then PATCH only totalprice to a new number. Assert totalprice changed AND every other field stayed the same.
Hint
Keep the original response body, then deep-compare every key except totalprice.
6PUT without auth fails
Create a booking, then PUT against its id WITHOUT the auth fixture. Assert the response is 403. This documents the auth contract.
Hint
Use the bare request fixture - no Cookie header.
7storageState reuse
Write global-setup.ts, run it, inspect the file .auth/state.json. Confirm it contains a single cookie named token.
Hint
Check the cookie domain matches restful-booker.herokuapp.com.
8Retry with backoff
Wrap a POST inside the retry() helper from section 11. Trigger failures by changing the path to /wrong first; confirm three attempts then throw.
Hint
Add console.log(i) inside the loop while debugging, then remove it.