Where this sits
The framework is finished. Fixtures, the base page, data-driven runs, the reporting, all of it works, and it has already run in Jenkins. So the batch turns to the last big block of the syllabus: API testing inside the same framework.
That last phrase is the point. Not a second tool, not a second repo, the same framework.
Still pending and being scheduled this week: Jenkins parts 3 and 4, and a GitHub Actions session. Separately, tonight there is an extra session on Introduction to Agent Skills, with a certification, alongside the Claude Code 101 and AI Fluency certificates the batch already has. All three take one to two hours together.
The three layers, and where an API sits
Every web and mobile application is three layers.
The analogy used in class: you are in a restaurant, the cook does not speak your language, so you need a waiter. You give the waiter an order, the waiter comes back with food. Request, response. The API is the waiter.
You can watch this happen. Open any site, DevTools, Network tab, submit a login with the wrong password, and you will see the request appear: a URL, a method, a payload with what you typed, and a status code coming back. That is the layer we are about to automate.
URL anatomy, and the two kinds of parameter
Worth being precise here, because interviewers ask it as a naming quiz.
https://restful-booker.herokuapp.com/booking/1
|____| |______________|_____________|________|
protocol subdomain main domain path param
|________________________|
base URL
- Path parameter starts with a slash:
/booking/1. It identifies a specific thing. - Query parameter starts with a question mark:
/booking?firstname=John. It filters. - Endpoint is the path after the base URL. Some people include the path parameter in that, some do not, and both readings are accepted.
The methods, and what each one is for
| Method | Job | On a bookings API |
|---|---|---|
GET | Fetch. One thing, or all of them | Read booking 101, or list every booking id |
POST | Create. Needs a payload | Create a new booking from a JSON body |
PUT | Full update. Send every field | Replace booking 101 entirely |
PATCH | Partial update. Send only what changes | Change only the first name on booking 101 |
DELETE | Remove | Delete booking 101 |
PUT against PATCH is the pair that gets asked. Full replace against partial edit.
Status codes come back in families: 1xx informational, 2xx success, 3xx redirect, 4xx you got it wrong, 5xx the server got it wrong. A 401 on a login means the credentials were rejected, and that is the server telling the client what happened.
Before you can write a single request
Five questions, every time:
- What is the base URL?
- Are there parameters, path or query?
- Is there a payload, and what shape is the JSON?
- What headers does it need? Usually
Content-Type: application/json. - Does it need authentication, a token or a key?
For the ping request the answers are: base URL yes, no params, no payload, no special header, no auth. That is why it is the one to start with.
Why Playwright and not Postman or Rest Assured
The question came up in class and it is a real interview question, so here is the honest comparison.
| Playwright | Postman | Rest Assured | |
|---|---|---|---|
| API and UI tests in one framework | Yes | No, they live apart | No, separate library |
| TypeScript | Native | Partial scripting | Java, not TS |
| Trace viewer | Yes | No | No |
| Reuses saved auth state | Yes | Manual | Manual |
| JSON schema validation | Yes, with a library | Partial | Needs a plugin |
| Cost in CI | Free | Licensed above a point | Free |
The first row is the one that matters. Your API tests and your UI tests live in the same repo, run in the same command, and report into the same trace. Everything else is a bonus.
That is not an argument against Postman. Postman is still where you do the manual exploration, and today's homework is a Postman collection. It is an argument against Postman as the place your automation lives.
The pyramid, and why API tests earn their place
The follow-up question in class was sharp: if the UI is already built, why not just test through the UI? Because a UI test is slow, brittle and expensive to maintain, and it exercises the same business rule that an API test can check in a fraction of the time. Test through the UI when the thing you are testing is the UI.
Manual first, then automate
Automation does not replace the manual pass, it consumes its output.
Asked in class: what if there is no documentation at all, no payload, nothing? Then you write the requirement yourself. Open the app, DevTools, Network tab, do the thing, then right click the request and Copy as cURL. Collect those, and you have a spec. It is a legitimate answer in an interview, and it is what people actually do.
The first request
import { test, expect } from '@playwright/test';
test('ping request', async ({ request }) => {
const response = await request.get('/ping');
expect(response.status()).toBe(201);
});
Three things to notice. There is no page fixture, because there is no browser and no DOM. There is no --headed, for the same reason. And request is a fixture Playwright already ships, so nothing needed installing.
request exposes get, post, put, patch and delete. That is the whole surface for now.
No tests found, and the config that fixes it
This is the part that ate ten minutes of class, and it is worth keeping because it will happen to you.
The test above sat in source/api/. Running it gave:
Error: No tests found.
Passing the full path to the file did not help. Saving again did not help. The file was fine.
The config's testDir pointed at source/test. Playwright only collects tests underneath testDir, so a spec outside it does not exist as far as the runner is concerned, no matter what path you type on the command line.
The fix is a config that switches on an environment variable, so one project serves both kinds of test:
import { defineConfig } from '@playwright/test';
const isApi = process.env.ENV === 'api';
export default defineConfig({
testDir: isApi ? './source/api' : './source/test',
use: isApi
? { baseURL: 'https://restful-booker.herokuapp.com' }
: {
baseURL: 'https://your-ui-under-test.example',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
});
Then run it with the variable set:
ENV=api npx playwright test
Running 1 test using 1 worker
1 passed (1.4s)
Two payoffs beyond just working. The API baseURL now lives in config, so specs hold /ping rather than the full URL. And ENV=api runs only the API directory, so a UI spec cannot accidentally join the run.
Screenshots and video are set only on the UI branch on purpose. API tests are blind: no browser, no page, nothing to photograph. Leaving those options on the API side costs you nothing but means nothing either. The trace still works, and the trace is what you actually want when a response body is wrong.
What the request fixture is doing
request, get an APIRequestContext, which issues the call and hands back a response to assert on.The status codes that will fail your first assertion
The class project is RESTful Booker, a hotel-booking API. It is a genuinely good thing to put on a CV if you have no API work to point at.
It is also full of status codes that are not what you would guess. Every row below was measured against the live API while writing these notes, not recalled:
| # | Request | Status | Watch out |
|---|---|---|---|
| 1 | GET /ping | 201 | A health check that returns Created. Not 200 |
| 2 | GET /booking | 200 | Returns an array of ids only, not full bookings |
| 3 | GET /booking/{id} | 200 | |
| 4 | POST /booking | 200 | A create that returns OK, not 201. No token needed |
| 5 | PUT /booking/{id} | 200 | 403 without a token |
| 6 | PATCH /booking/{id} | 200 | 403 without a token |
| 7 | DELETE /booking/{id} | 201 | 403 without a token, and a delete that returns Created |
Assume 200 on the ping and the run fails immediately:
Error: expect(received).toBe(expected)
Expected: 200
Received: 201
Three of the seven need a token, and the class did not get to that part. Creating a booking needs nothing, which is what was shown. But PUT, PATCH and DELETE all return 403 Forbidden on their own. Get a token first, then send it as a cookie:
POST /authwith{"username":"admin","password":"password123"}returns{"token":"..."}, and the write requests take it as aCookie: token=...header. If three of your seven come back 403 tonight, that is why, and it is the API behaving correctly rather than you doing it wrong.
Tasks and announcements
Today's task. Install Postman, create a fresh workspace, and import all seven RESTful Booker requests into one collection using Copy as cURL and Postman's Import button. Name them 01 ping, 02 get all, and so on. Next class automates this collection, so come with it built.
- Build it yourself. The finished collection was deliberately not shared. It gets shared next class, after everyone has made their own.
- Watch the two web fundamentals videos if you have not. Recommended for everyone, not just manual testers.
- Tonight: Introduction to Agent Skills, with certification. If Claude Code 101 and AI Fluency are still outstanding, all three together are one to two hours.
- Coming next: the full CRUD run, then an API helper utility, then API fixtures, then JSON path and schema validation. Five pieces in total, and the framework is only finished when the fixtures land.
- Pending and being scheduled this week: Jenkins parts 3 and 4, plus GitHub Actions.
- Questions go on SDET Club rather than chat, tagged, so they get a proper answer.