# API Testing

Course: [Playwright](https://www.devtutorials.ai/learn/playwright) · Section: Authentication, Network and APIs
Source: https://www.devtutorials.ai/learn/playwright/auth-network-api/api-testing

> Why test APIs directly, the request fixture, sending GET, POST, PUT, PATCH and DELETE requests with JSON, form data, query params and headers, checking status, headers and JSON with toBeOK and toMatchObject, API authentication with tokens and cookies, a full create-read-update-delete flow, error cases, separate request contexts, and mixing API and UI steps in one test.

## API Testing

- Behind most web pages is an **API**: a set of URLs that send and receive data, usually as JSON.
- Playwright can call that API directly, with no browser page at all. These tests are very fast.
- In this lesson, you will test the Practice Shop's API, and use it to set up data for UI tests.

**Simple lesson:** an API test sends a request and checks the answer. No page, no clicks.

### A Real-World Example

- The shop's API has a product endpoint: `GET /api/products/2`.
- This test asks for product 2 and checks the answer. It needs no page, so it finishes in milliseconds.

- The test uses the `request` fixture instead of `page`. No browser page opens.

### What You Will Learn

1. Why API tests are useful, and how they fit with UI tests.
2. The `request` fixture and the five common methods.
3. Checking status codes, headers and JSON.
4. Sending JSON, form data, query params and headers.
5. Logging in to an API with a token or a cookie.
6. A full create, read, update, delete flow, with error cases.
7. Using the API to prepare data for UI tests.

## Why Test the API

### Faster and More Focused

- A UI test loads pages, waits for elements and clicks. An API test sends one request.
- API tests are often **10 to 100 times faster**. You can run many more of them.
- They check the **rules** of the app directly: prices, validation, permissions.
- UI tests are still needed. They check what the user actually sees and does.

### The Shop's API

| Method | URL | What it does |
|---|---|---|
| `GET` | `/api/products` | List products. `?category=Fitness` filters them |
| `GET` | `/api/products/:id` | One product |
| `POST` | `/api/login` | Log in. Returns a token and sets a cookie |
| `GET` | `/api/me` | The logged-in user |
| `GET` | `/api/orders` | Your orders |
| `POST` | `/api/orders` | Place an order |
| `GET`, `PATCH`, `PUT`, `DELETE` | `/api/orders/:id` | Read, change, replace or delete one order |

**Simple lesson:** test the rules through the API. Test what users see through the UI.

## The request Fixture

### Sending Requests

- `request.get(url)`, `request.post(url)`, `request.put(url)`, `request.patch(url)` and `request.delete(url)` send the five common methods.
- Relative URLs use `baseURL` from the config, just like `page.goto()`.
- Each call returns an **API response**.

<figure className="plw-fig">
<svg viewBox="0 0 640 190" role="img" aria-labelledby="fig-plw-api" style={{ width: "100%", maxWidth: 640, height: "auto" }}>
  <title id="fig-plw-api">Animation: the test sends POST /api/orders with a JSON body straight to the server. The server answers 201 Created with the new order as JSON. No browser page takes part</title>
  <rect x="15" y="45" width="160" height="100" rx="12" style={{ fill: "var(--plw-purple-soft)", stroke: "var(--plw-purple)" }} />
  <text x="95" y="85" textAnchor="middle" style={{ fill: "var(--plw-purple)", fontFamily: "var(--sans)", fontSize: 14, fontWeight: 700 }}>Your test</text>
  <text x="95" y="107" textAnchor="middle" style={{ fill: "var(--ink2)", fontFamily: "var(--mono)", fontSize: 11 }}>request.post()</text>
  <rect x="465" y="45" width="160" height="100" rx="12" style={{ fill: "var(--plw-green-soft)", stroke: "var(--plw-green)" }} />
  <text x="545" y="85" textAnchor="middle" style={{ fill: "var(--plw-green)", fontFamily: "var(--sans)", fontSize: 14, fontWeight: 700 }}>Server API</text>
  <text x="545" y="107" textAnchor="middle" style={{ fill: "var(--ink2)", fontFamily: "var(--mono)", fontSize: 11 }}>/api/orders</text>
  <g style={{ fontFamily: "var(--mono)", fontSize: 11 }}>
    <g><animateTransform attributeName="transform" type="translate" values="0 0;270 0;270 0" keyTimes="0;0.35;1" dur="7s" repeatCount="indefinite" /><animate attributeName="opacity" values="1;1;0;0" keyTimes="0;0.35;0.38;1" dur="7s" repeatCount="indefinite" /><rect x="180" y="20" width="190" height="22" rx="5" style={{ fill: "var(--plw-blue)" }} /><text x="190" y="35" style={{ fill: "#fff" }}>POST {"{"} productId: 1 {"}"}</text></g>
    <g opacity="0"><animateTransform attributeName="transform" type="translate" values="0 0;0 0;-270 0;-270 0" keyTimes="0;0.42;0.75;1" dur="7s" repeatCount="indefinite" /><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.41;0.42;0.96;1" dur="7s" repeatCount="indefinite" /><rect x="270" y="150" width="190" height="22" rx="5" style={{ fill: "var(--plw-green)" }} /><text x="280" y="165" style={{ fill: "#fff" }}>201 {"{"} id: 1004, … {"}"}</text></g>
  </g>
</svg>
<figcaption>An API test talks to the server directly. There is no page in between.</figcaption>
</figure>

### Checking the Response

- `await expect(response).toBeOK()` passes for any status from 200 to 299.
- `response.status()` gives the exact code. `response.json()` reads the body. `response.headers()` gives the headers, with lower-case names.
- `toMatchObject()` checks some fields and ignores the rest. `expect.any(Number)` accepts any number, for IDs you cannot know in advance.

- `params` adds the query string for you: `?category=Fitness`.

`toBeOK()` is a web-first style matcher, so write `await` before it. On failure it prints the status and the start of the body, which often explains the problem.

**Simple lesson:** `toBeOK()` for "it worked", `status()` for the exact code, `json()` for the data.

## Sending Data

### JSON, Forms and Headers

- `data: { ... }` sends a JSON body and sets `Content-Type: application/json`.
- `form: { ... }` sends a classic HTML form body.
- `headers: { ... }` adds headers to one request.
- The shop's login accepts both JSON and forms.

## Authenticated APIs

### Tokens in Headers

- Many APIs want a **token** in the `Authorization` header: `Bearer <token>`.
- Set it once for a whole file with `test.use({ extraHTTPHeaders: { ... } })`, or for the whole project in the config's `use`.

- In a real project, the token comes from the login call or an environment variable, never written in the test.

### Cookies Carry Over

- The `request` fixture keeps **cookies** between calls in one test, like a browser.
- Log in once with `POST /api/login`, and later calls are logged in through the cookie.

- Since Playwright 1.64, a request context can also manage cookies directly: `request.addCookies([...])`, `request.cookies()` and `request.clearCookies()`, like a browser context.

**Simple lesson:** send a token in a header, or log in once and let the cookie do the rest.

## A Full CRUD Flow

### Create, Read, Update, Delete

- **CRUD** means create, read, update and delete. Most APIs follow this pattern.
- `POST` creates and usually answers **201 Created**. `GET` reads. `PATCH` changes some fields. `PUT` replaces the whole thing. `DELETE` removes it, often with **204 No Content**.
- This test follows one order through its whole life.

### Testing the Error Cases

- Good API tests check the **unhappy** paths too. They are where bugs hide.
- **400** or **422**: bad input. **401**: not logged in. **403**: logged in, but not allowed. **404**: not found. **409**: a conflict, like changing a delivered order.

Tests that create data should clean it up, or use data that cannot clash. The practice API resets for every test. A real test server usually does not, so delete what you create, often in `afterEach`.

**Simple lesson:** test each status code your API promises, not only 200.

## Request Contexts and Options

### Your Own Request Context

- The `request` fixture is made fresh for each test.
- For setup work outside a test, like `beforeAll`, make your own with `request.newContext()` from `@playwright/test`.
- Give it a `baseURL` and headers. Call `dispose()` when done.

### failOnStatusCode

- By default, a 404 or 500 does **not** throw. You check the status yourself.
- `failOnStatusCode: true` makes any non-2xx status throw at once, with the status in the error.
- Use it for setup calls that must work. Leave it off when you are testing error codes.

A setup call that fails quietly makes the real check fail later, with a confusing message. `failOnStatusCode: true` stops the test at the call that broke.

## Mixing API and UI

### API for Setup, UI for the Check

- Creating test data through the UI is slow. Creating it through the API is fast.
- `page.request` is a request context that **shares cookies** with the page. Log in or create data with it, then open the page.

- The reverse also works: act in the UI, then check the result through the API.

**Simple lesson:** set up with the API, check with the UI. Each part does what it is best at.

## Try It in Your Project

- These steps take the lesson from the lab to your own app.
- Write API tests for your three most important endpoints, including one error status each.
- Replace one slow UI data setup with an API call through `page.request`.
- Clean up the data your tests create, or use unique names per run.

**Simple lesson:** use one idea from this lesson on your own app this week.

## TL;DR

- The `request` fixture sends `get`, `post`, `put`, `patch` and `delete` with no page.
- `await expect(response).toBeOK()`, `status()`, `headers()` and `json()` check the answer.
- `data` sends JSON, `form` sends forms, `params` builds the query string.
- Tokens go in `extraHTTPHeaders`. Login cookies carry over between calls.
- Test every status code: 201, 204, 401, 403, 404, 409, 422.
- `page.request` shares cookies with the page. Use it to set up data for UI tests.

## Common mistakes

| Mistake | What happens | Fix |
|---|---|---|
| `expect(response).toBeOK()` without `await` | The check may not run before the test ends | Write `await expect(response).toBeOK()` |
| Only testing 200 answers | Validation and permission bugs go unseen | Add tests for 4xx codes |
| Expecting a 404 to throw | It does not. The test carries on with bad data | Check `status()`, or use `failOnStatusCode: true` |
| Checking the whole JSON with `toEqual` when IDs change | The test breaks on every run | `toMatchObject` with `expect.any(Number)` |
| Tokens written in the test file | Secrets leak into git | Read them from environment variables |

## Check Yourself

## Exercises

## What to Remember

- `request` sends API calls with no page. They are fast and focused.
- `await expect(response).toBeOK()`, then `status()` and `json()`.
- `data`, `form`, `params` and `headers` shape the request.
- Tokens in `extraHTTPHeaders`. Cookies carry over after a login.
- Test the error codes, and clean up data you create.
- API for setup, UI for what users see.

Next: **Page Object Model**
