1. Create, read, update, delete
Intermediate
Log in for a token, then take a user through its whole life cycle in your own sandbox.
Expected: login returns 200 with a token; creating returns 201 and the new user with an id; reading it back returns the same data; PATCH changes only the job; DELETE returns 204; reading it again returns 404.
Show me how
import { test, expect } from '@playwright/test';
// A unique sandbox per run: your data never collides with anyone else's.
const sandbox = `pw-${Date.now()}`;
test('takes a user through create, read, update and delete', async ({ request }) => {
const headers = { 'X-Sandbox-Id': sandbox };
// "data" is sent as JSON, with the Content-Type header set for you.
const login = await request.post('/api/auth/login', {
headers,
data: { username: 'practice', password: 'SuperSecret123' },
});
expect(login.status()).toBe(200);
const { token } = await login.json();
const auth = { ...headers, Authorization: `Bearer ${token}` };
const created = await request.post('/api/users', {
headers: auth,
data: { name: 'Jane Doe', email: '[email protected]', job: 'SDET' },
});
expect(created.status()).toBe(201);
const user = await created.json();
expect(user).toMatchObject({ name: 'Jane Doe', email: '[email protected]', job: 'SDET' });
expect(user.id).toEqual(expect.any(Number));
const read = await request.get(`/api/users/${user.id}`, { headers });
expect(await read.json()).toEqual(user);
const patched = await request.patch(`/api/users/${user.id}`, { headers: auth, data: { job: 'Lead SDET' } });
expect(await patched.json()).toMatchObject({ name: 'Jane Doe', job: 'Lead SDET' });
const deleted = await request.delete(`/api/users/${user.id}`, { headers: auth });
expect(deleted.status()).toBe(204);
const gone = await request.get(`/api/users/${user.id}`, { headers });
expect(gone.status()).toBe(404);
});
2. Error responses
Intermediate
Check that the API refuses bad requests with the right status code and a useful body.
Expected: creating a user without a token → 401; with an invalid email and a one-letter name → 422 with a message per field in fields; creating the same email twice → 409; a wrong password at login → 401.
Show me how
import { test, expect, type APIRequestContext } from '@playwright/test';
const headers = { 'X-Sandbox-Id': `pw-errors-${Date.now()}` };
async function authHeaders(request: APIRequestContext) {
const res = await request.post('/api/auth/login', {
headers,
data: { username: 'practice', password: 'SuperSecret123' },
});
const { token } = await res.json();
return { ...headers, Authorization: `Bearer ${token}` };
}
test('rejects bad requests with clear errors', async ({ request }) => {
const noToken = await request.post('/api/users', { headers, data: { name: 'Jo', email: '[email protected]' } });
expect(noToken.status()).toBe(401);
const badLogin = await request.post('/api/auth/login', { headers, data: { username: 'practice', password: 'nope' } });
expect(badLogin.status()).toBe(401);
const auth = await authHeaders(request);
const invalid = await request.post('/api/users', { headers: auth, data: { name: 'J', email: 'not-an-email' } });
expect(invalid.status()).toBe(422);
// Check the shape of the body, not just the code: clients rely on it.
expect(await invalid.json()).toMatchObject({
error: 'Validation failed.',
fields: { name: expect.any(String), email: expect.any(String) },
});
const user = { name: 'Twin', email: `twin-${Date.now()}@example.com` };
expect((await request.post('/api/users', { headers: auth, data: user })).status()).toBe(201);
const duplicate = await request.post('/api/users', { headers: auth, data: user });
expect(duplicate.status()).toBe(409);
});
3. Pagination and search
Intermediate
Read the user list a page at a time, then search it.
Expected: with limit=5 a fresh sandbox reports total: 12 and totalPages: 3; walking all pages returns 12 unique users; every result for search=qa contains "qa" in its name, email or job.
Show me how
import { test, expect } from '@playwright/test';
test('pages through and searches the users', async ({ request }) => {
// A brand-new sandbox always starts with the same 12 seed users.
const headers = { 'X-Sandbox-Id': `pw-paging-${Date.now()}` };
const first = await (await request.get('/api/users', { headers, params: { limit: 5 } })).json();
expect(first).toMatchObject({ page: 1, limit: 5, total: 12, totalPages: 3 });
const ids: number[] = [];
for (let page = 1; page <= first.totalPages; page++) {
// "params" builds the query string for you: ?page=2&limit=5
const res = await request.get('/api/users', { headers, params: { page, limit: 5 } });
ids.push(...(await res.json()).data.map((u: { id: number }) => u.id));
}
expect(new Set(ids).size).toBe(12);
const search = await (await request.get('/api/users', { headers, params: { search: 'qa' } })).json();
expect(search.total).toBeGreaterThan(0);
for (const u of search.data) {
expect(`${u.name} ${u.email} ${u.job}`.toLowerCase()).toContain('qa');
}
});
4. Status codes and timeouts
Advanced
Use /api/status/{code} and /api/delay/{seconds} to test how your client handles failures and slow servers.
Expected: each status code you ask for comes back unchanged; a 3-second delay with a 1-second client timeout fails with a timeout error; with a 5-second timeout the same request succeeds.
Show me how
import { test, expect } from '@playwright/test';
for (const code of [200, 404, 418, 500, 503]) {
test(`returns status ${code}`, async ({ request }) => {
const res = await request.get(`/api/status/${code}`);
expect(res.status()).toBe(code);
// ok() is true only for 2xx codes.
expect(res.ok()).toBe(code === 200);
});
}
test('times out on a slow response', async ({ request }) => {
// A rejected promise is asserted with "rejects", not try/catch.
await expect(request.get('/api/delay/3', { timeout: 1_000 })).rejects.toThrow(/Timeout/);
const slow = await request.get('/api/delay/3', { timeout: 5_000 });
expect(await slow.json()).toEqual({ delayedSeconds: 3 });
});