Find API Defects with Playwright Python
Find an API defect in Postman and prevent it with Playwright Python tests.
Introduction
30 Second Summary
A form can look like it worked while quietly accepting data that should have been rejected. That hidden mistake often survives until the same bad input causes trouble again.
In this project, you will build a local Task API with health checks plus task creation. You will trace an empty-title defect from manual discovery in Postman to a passing regression test in Playwright for Python.
What You'll Build
Your final terminal run shows three passing tests after the empty-title check exposes the original defect.
By the end of this project, you'll have:
- A working Task API where you can send GET /health to see a 200 JSON response. You can send POST /tasks with a title to receive a created task.
- A repeatable Postman defect report that proves an empty title incorrectly receives status 201. It also records the expected API contract for a 400 error response.
- An automated Playwright regression suite covering the health response, valid task creation, plus empty-title rejection. You can watch the negative test fail before the fix. A final rerun shows all three tests passing.
- Secret Mission: Expand the suite across four invalid-title equivalence partitions covering a missing field, an empty string, whitespace-only text, plus a number.
Are there any prerequisites?
You'll need a Windows 11 computer with Visual Studio Code. You'll also need internet access for the initial downloads plus basic familiarity with Python functions and dictionaries.
Before We Start
Before setup begins, commit to turning an empty-title defect discovered in Postman into a Playwright Python regression test run with pytest. The expected API contract rejects an empty title because every task needs a meaningful non-empty string title.
Set Up the Python API Test Lab
Your API test lab needs known tool versions before you can trust its results. An isolated setup keeps the tests reproducible throughout the project.
Python 3.14.8 supplies the local server runtime. The Postman desktop app gives you a quick way to inspect requests without creating an account.
A virtual environment in Visual Studio Code will hold Playwright for Python 1.63.0, pytest 9.1.1, plus pytest-playwright 0.10.0.
In this step, get ready to:
- Install Python 3.14.8 for the local API lab.
- Open Postman desktop in lightweight API Client mode.
- Create an isolated VS Code workspace with the pinned test packages.
Install Python 3.14.8
The Python Install Manager manages Python runtimes on Windows. You will use a version-specific check so the lab starts with the required runtime.
- Press the Windows key to open Windows Search.
- Type Command Prompt.
- Press Enter to open Command Prompt.
- Check for the required Python runtime by running this command:
py -V:3.14 --version
What does this command check?
- The py command uses the Python Install Manager.
- The -V:3.14 selector targets the Python 3.14 runtime family.
- The --version option prints the installed runtime version.
✔️ I see Python 3.14.8
That is the exact runtime this lab needs. Your Python installation is ready for the local API server.
ⓧ I see an older version
- Press the Windows key to open Windows Search.
- Type Python Install Manager.
- Press Enter to open the manager.
- Select the current Python 3.14 runtime.
- Click Install.
- Return to Command Prompt.
- Check the updated runtime by running this command:
py -V:3.14 --version
What confirms the update?
The command asks the manager for the installed Python 3.14 runtime. You are ready when the output says Python 3.14.8.
ⓧ Python 3.14 is unavailable
- Open the official Python downloads page.
- Download the Python Install Manager installer.
- Run the downloaded installer.
- Approve the Windows permission prompt if it appears.
- Click Install.
- Press the Windows key to open Windows Search.
- Type Python Install Manager.
- Press Enter to open the manager.
Python Install Manager is now available. It can install the runtime required by this lab.
- Select the Python 3.14 runtime.
- Click Install.
- Return to Command Prompt.
- Confirm the installed runtime by running this command:
py -V:3.14 --version
What confirms the installation?
The version check targets the runtime you just installed. You are ready when the output says Python 3.14.8.
Install Postman desktop
Postman provides the manual side of this testing workflow. Its lightweight API Client stores your work locally without requiring an account.
- Press the Windows key to open Windows Search.
- Type Postman.
- Choose the tab below that matches your search result.
✔️ Postman is installed
- Select the Postman desktop app from the search results.
- Continue with the lightweight API Client option.
Postman opens without requiring an account. Your local API client is ready for the requests you will send later.
ⓧ Postman is not installed
- Open the official Postman downloads page.
- Select Windows Intel 64-bit or ARM 64-bit to match your computer.
- Run the downloaded .exe file.
- Approve the Windows permission prompt if it appears.
- Wait for the Postman desktop app to open.
- Continue with the lightweight API Client option.
The lightweight client keeps this lab local. You can create requests without signing in.
That removes account setup from your testing path. Postman is now ready to inspect the Task API in the next step.
Build the isolated workspace
The .venv folder keeps this project's packages separate from other Python projects. The requirements.txt file records the exact versions needed to reproduce the lab.
- Press the Windows key to open Windows Search.
- Type File Explorer.
- Press Enter to open File Explorer.
- Select Desktop in the left navigation.
- Click New in the toolbar.
- Select Folder.
- Enter playwright-api-lab.
- Press Enter to create the folder.
You should see the empty playwright-api-lab folder on your Desktop. This gives every project file one predictable location.
- Press the Windows key to open Windows Search.
- Type Visual Studio Code.
- Press Enter to open VS Code.
- Click File in the top menu.
- Select Open Folder.
- Choose the playwright-api-lab folder from your Desktop.
- Click Select Folder.
VS Code now shows playwright-api-lab in the Explorer sidebar. Any terminal you open here starts inside that folder.
- Click the New File button in the Explorer sidebar.
- Enter requirements.txt.
- Press Enter to create the file.
The blank requirements.txt file opens in the editor. It will pin every package used by the automated tests.
- Add the pinned dependencies to requirements.txt by copying the code below:
playwright==1.63.0
pytest==9.1.1
pytest-playwright==0.10.0
What does this file control?
- The playwright==1.63.0 line installs Playwright's Python package at the version used by this project.
- The pytest==9.1.1 line installs the test runner.
- The pytest-playwright==0.10.0 line connects Playwright with pytest fixtures.
- Save requirements.txt by pressing Cmd+S (macOS) or Ctrl+S (Windows).
- Confirm that the file contains exactly three package lines.
Package pins look different?
Check each package name for missing hyphens. Confirm that every version uses two equals signs.
Ask for help if the file still differs. Help me compare my requirements.txt package pins with the expected Playwright and pytest versions.
✔️ Awesome, I've got everything!
- Keep requirements.txt saved in the playwright-api-lab folder.
ⓧ I'd like to double check the full code
- Compare your requirements.txt file with this complete reference:
playwright==1.63.0
pytest==9.1.1
pytest-playwright==0.10.0
How to use this reference
These are the complete contents of requirements.txt for this step. Match the package names plus their pinned versions exactly.
- Click Terminal in the VS Code top menu.
- Select New Terminal.
- Click the profile menu in the Terminal panel.
- Select Command Prompt.
- Create the isolated environment by running this command:
python -m venv .venv
What does this command create?
The venv module creates an isolated Python environment inside .venv. Packages installed there stay scoped to this lab.
- Confirm that .venv appears in the VS Code Explorer sidebar.
The .venv folder is missing?
Confirm that the terminal path points to playwright-api-lab. Check that the Python version command succeeded earlier.
Use this prompt for focused help. Help me find why python -m venv .venv did not create the environment in my VS Code workspace.
- Activate the virtual environment in Command Prompt by running this command:
.venv\Scripts\activate
What does activation change?
Activation makes python use the interpreter inside .venv. New packages now install into this project environment.
- Confirm that the terminal prompt begins with (.venv).
The prompt does not show (.venv)?
Confirm that the terminal profile says Command Prompt. Check that the .venv folder exists before running the activation command again.
Use this prompt if activation still fails. Help me activate my .venv from a Command Prompt terminal in VS Code.
- Install the pinned project packages by running this command:
py -m pip install -r requirements.txt
What does this install?
The -r requirements.txt option tells pip to read the package list from your saved file. The version pins keep every installation consistent.
- Wait for the terminal to return to the (.venv) prompt.
- Confirm that the installation output mentions playwright, pytest, plus pytest-playwright.
Package installation failed?
Confirm that requirements.txt is saved inside playwright-api-lab. Check that the terminal prompt still begins with (.venv).
Use this prompt to diagnose the installation output. Help me troubleshoot my pinned Playwright and pytest package installation.
Before you inspect the environment, do you expect every installed version to match its line in requirements.txt?
- Inspect the installed package versions by running this command:
py -m pip show playwright pytest-playwright pytest
What does this output prove?
The command prints package metadata with Name: plus Version: fields. Those fields let you compare the installed environment with requirements.txt.
You should see Version: 1.63.0, Version: 0.10.0, plus Version: 9.1.1 in the output. That confirms the isolated test environment matches every required package pin.
Your Python API test lab is ready. Next, you will launch a local Task API and retrieve its first JSON health response.
Launch a Naive Task API
Your Python test lab is ready. Automation now needs a real local API that can return a response you can inspect.
This step launches a small Task API that you can check in Postman. Its first task-creation path trusts each incoming title to set up the defect you will investigate next.
In this step, get ready to:
- Build a local Task API with health and task-creation routes.
- Run the deliberately naive server from your activated virtual environment.
- Send a health request from Postman to confirm the API is reachable.
Build the health endpoint
A health endpoint answers a simple question: is the service reachable? It returns a small JSON body over HTTP so Postman has a concrete baseline.
- Use the Explorer sidebar in Visual Studio Code to create server.py inside playwright-api-lab.
- Add the server configuration plus the health route by pasting this code into server.py:
import json
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
HOST = "localhost"
PORT = 3000
TASKS = []
class TaskHandler(BaseHTTPRequestHandler):
def _send_json(self, status_code, payload):
body = json.dumps(payload).encode("utf-8")
self.send_response(status_code)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def do_GET(self):
if self.path == "/health":
self._send_json(200, {"status": "ok"})
return
self._send_json(404, {"error": "not found"})
How does the health path work?
- The HOST value keeps the server on your computer. The PORT value gives it a predictable local address.
- The _send_json helper converts a Python value into a JSON response with the correct headers.
- The do_GET method returns the health response when the request path is /health.
- Any other GET path receives a 404 response so unsupported routes are visible.
- Save server.py.
- Confirm server.py appears beside requirements.txt in the Explorer sidebar.
Can't find server.py?
- Confirm the Explorer sidebar still shows the playwright-api-lab folder.
- Move server.py into playwright-api-lab if it was created elsewhere.
Help me check why server.py is missing from my project folder.
Add naive task creation
The POST /tasks route receives a JSON object and stores a task in memory. This first version copies title into TASKS without checking its value.
- In server.py, locate the final line containing self._send_json(404, {"error": "not found"}).
- Press Enter twice to leave one blank line below it.
- Add the naive creation route plus the server entry point by pasting this code below the blank line:
def do_POST(self):
if self.path != "/tasks":
self._send_json(404, {"error": "not found"})
return
content_length = int(self.headers.get("Content-Length", "0"))
raw_body = self.rfile.read(content_length)
try:
payload = json.loads(raw_body)
except (json.JSONDecodeError, UnicodeDecodeError):
self._send_json(400, {"error": "invalid JSON"})
return
title = payload.get("title") if isinstance(payload, dict) else None
task = {"id": len(TASKS) + 1, "title": title}
TASKS.append(task)
self._send_json(201, task)
if __name__ == "__main__":
server = ThreadingHTTPServer((HOST, PORT), TaskHandler)
print(f"Task API running at http://{HOST}:{PORT}")
server.serve_forever()
What does the creation path do?
- The do_POST method accepts task creation requests at /tasks.
- The request body is read using its content length. The bytes are then decoded as JSON.
- Malformed JSON receives a 400 response before task creation begins.
- The title value is copied directly into a new task. The task receives the next numeric ID.
- The __main__ block starts ThreadingHTTPServer with TaskHandler handling each request.
- Save server.py.
- Confirm do_POST aligns with do_GET inside TaskHandler.
- Confirm the __main__ conditional begins at the far-left edge of server.py.
Does the indentation look uneven?
- Align def do_POST with def do_GET so both methods belong to TaskHandler.
- Move if __name__ == "__main__": to the far-left edge so it stays outside the class.
Help me fix the indentation in server.py.
✔️ Awesome, I've got everything!
Your complete naive Task API is ready. Double-check that you saved server.py.
ⓧ I'd like to double check the full code
import json
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
HOST = "localhost"
PORT = 3000
TASKS = []
class TaskHandler(BaseHTTPRequestHandler):
def _send_json(self, status_code, payload):
body = json.dumps(payload).encode("utf-8")
self.send_response(status_code)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def do_GET(self):
if self.path == "/health":
self._send_json(200, {"status": "ok"})
return
self._send_json(404, {"error": "not found"})
def do_POST(self):
if self.path != "/tasks":
self._send_json(404, {"error": "not found"})
return
content_length = int(self.headers.get("Content-Length", "0"))
raw_body = self.rfile.read(content_length)
try:
payload = json.loads(raw_body)
except (json.JSONDecodeError, UnicodeDecodeError):
self._send_json(400, {"error": "invalid JSON"})
return
title = payload.get("title") if isinstance(payload, dict) else None
task = {"id": len(TASKS) + 1, "title": title}
TASKS.append(task)
self._send_json(201, task)
if __name__ == "__main__":
server = ThreadingHTTPServer((HOST, PORT), TaskHandler)
print(f"Task API running at http://{HOST}:{PORT}")
server.serve_forever()
Launch and inspect the API
The entry point creates a local server process that keeps listening for requests. The activated .venv ensures the project uses the Python environment from the previous step.
- Switch back to the activated Command Prompt terminal from earlier.
Before you start the server, what local address do you expect it to print? Hold your prediction in mind.
- Start the Task API by running this command:
python server.py
What does this command do?
This command executes server.py with the active Python interpreter. The server keeps control of the terminal while it listens for local requests.
You will see Task API running at http://localhost:3000 in the terminal. Keep that process running while you send the request from Postman.
Server not staying online?
- Confirm the terminal is inside the playwright-api-lab folder.
- Confirm server.py is saved before running the command again.
- Close any other local process using port 3000 if the server reports that the address is unavailable.
Help me diagnose why python server.py does not keep my Task API running.
- Return to the Postman lightweight API Client from earlier.
- Click New.
- Select HTTP.
- Choose GET from the request method menu.
- Enter http://localhost:3000/health in the request URL field.
Before you send the request, what status and JSON body do you expect from the health route? Keep your prediction in mind.
- Click Send.
You will see status 200 with the response body {"status": "ok"}. This proves Postman can reach your local Task API.
Health request not succeeding?
- Confirm the terminal still shows the running server process.
- Confirm the request method is GET.
- Confirm the request URL ends with /health.
Help me troubleshoot my GET /health request.
That first live response proves your local API is reachable. Next, you will use Postman to test task creation and expose what this naive version accepts.
Expose the Validation Defect in Postman
Your Postman health check proved that the Task API is reachable. A healthy server can still accept data that violates its rules.
This step compares a valid task with an empty-title task. The contrast exposes the missing validation before you automate the expected API contract.
In this step, get ready to:
- Create a valid task through POST /tasks.
- Expose the validation defect with an empty title.
- Define the expected contract for invalid titles.
Send a valid task request
A positive request with a JSON body establishes the API's current success shape. You need this baseline before invalid input has meaning.
- Switch back to Postman.
- Change the request method selector to POST.
- Replace the request URL with http://localhost:3000/tasks.
- Select the Body tab.
- Select raw.
- Choose JSON from the format list.
- Set the request body by pasting the JSON below:
{"title": "Learn API testing"}
What Does This Request Body Do?
- The title field supplies the task description expected by POST /tasks.
- The value Learn API testing gives the API a non-empty title to store.
- Click Send.
You'll see status 201. The response body contains a numeric id plus the title Learn API testing.
Good work. This successful response gives you a baseline for valid task creation.
Valid Task Request Not Working?
- Confirm the Command Prompt terminal still shows that the Task API is running at http://localhost:3000.
- Check that the request method is POST.
- Check that the request URL ends with /tasks.
- Confirm that JSON remains selected for the raw body format.
Help me diagnose why my valid Postman request is not creating a task.
Probe the empty-title case
Negative testing checks how an API responds to input that breaks its rules. An empty title reveals whether the create endpoint enforces its most basic requirement.
- Return to the Body tab.
- Replace the current request body with the JSON below:
{"title": ""}
Why Does This Input Matter?
This body is valid JSON. Its title value cannot describe a task because the string contains no characters.
Before you send it, consider this question: will the API reject the empty title or create another task.
- Click Send.
You'll see status 201. The response body contains a numeric id with an empty title value.
The request succeeded at the HTTP level while violating the task rule. This is the validation defect you were looking for.
Do You See a Different Result?
- Confirm the latest request body is exactly {"title": ""}.
- Confirm the response panel belongs to your latest POST /tasks request.
- Check that the running server.py still creates a task immediately after assigning title.
Help me work out why my empty-title request does not reproduce the expected defect.
Turn the Failure into a Contract
An API contract defines the responses clients can expect from an endpoint. This contract groups invalid titles into four equivalence partitions.
- A missing title field is invalid.
- An empty string is invalid.
- A whitespace-only string is invalid.
- A non-string title is invalid.
Every invalid case should return status 400 with body {"error": "title must be a non-empty string"}.
- Document the expected invalid-title contract in the checkpoint below.
You found the defect that the happy path concealed. The expected behavior is now precise enough to automate.
Next, you'll convert this manual discovery into a repeatable Playwright regression test.
Capture the Contract in Playwright Tests
Your Postman requests proved that the naive API can create a valid task. They also exposed its empty-title defect.
Manual checks are easy to forget. Automated API contract tests preserve the same expectations for every run.
Playwright sends the API requests from Python. Pytest runs the three checks as a repeatable suite.
In this step, get ready to:
- Create a reusable Playwright request fixture.
- Automate the health and valid task-creation contracts.
- Preserve the empty-title defect as a failing regression test.
Build the reusable request fixture
A pytest fixture prepares reusable test state. A session-scoped fixture shares one API request context across the suite.
- Switch back to Visual Studio Code.
- Create a tests folder inside playwright-api-lab using the left file sidebar.
- Create test_tasks_api.py inside tests using the same sidebar.
- Add the imports and base URL to tests/test_tasks_api.py by pasting this code:
import pytest
from playwright.sync_api import APIRequestContext, Playwright
BASE_URL = "http://localhost:3000"
What does this setup do?
- The pytest import provides the fixture decorator used by the shared setup.
- The Playwright import provides the request context type plus the fixture type supplied by the plugin.
- BASE_URL keeps every request connected to the local Task API.
- Save tests/test_tasks_api.py.
- Confirm tests/test_tasks_api.py appears beneath the tests folder in the file sidebar.
Are the Playwright imports underlined?
Confirm Visual Studio Code is using the Python interpreter from .venv. The pinned packages were installed into that environment.
Ask for help checking the interpreter if the imports remain unresolved: Help me connect Visual Studio Code to the Python virtual environment in my playwright-api-lab folder.
- Add the shared fixture below BASE_URL by pasting this code:
@pytest.fixture(scope="session")
def api_request_context(playwright: Playwright):
request_context = playwright.request.new_context(base_url=BASE_URL)
yield request_context
request_context.dispose()
How does the fixture work?
- The session scope creates one request context for the complete test run.
- The request context combines relative request paths with BASE_URL.
- The yield statement supplies the context to each test.
- The final disposal call releases the context after the suite finishes.
- Save tests/test_tasks_api.py.
- Confirm api_request_context creates the context before yield.
- Confirm api_request_context disposes the context after yield.
Does the fixture show a syntax problem?
Check that the decorator begins with @pytest.fixture. Keep every line inside api_request_context indented by four spaces.
Ask for help comparing the fixture if the warning remains: Help me find the syntax problem in my Playwright API request fixture.
Add the positive contract tests
The first two tests capture behavior that already works. One checks the health response while the other checks valid task creation.
- Add the health test below api_request_context by pasting this code:
def test_health(api_request_context: APIRequestContext):
response = api_request_context.get("/health")
assert response.status == 200
assert response.json() == {"status": "ok"}
What does the health test prove?
- The test uses the shared request context to send a GET request to /health.
- The first assertion checks the successful 200 status.
- The second assertion checks the complete JSON response body.
- Save tests/test_tasks_api.py.
- Create a second Command Prompt terminal in Visual Studio Code.
- Activate the .venv from Step 1 in this second terminal.
- Run the health test against the still-running API by entering this command:
python -m pytest
You'll see the health test pass. Your first automated API contract now checks a real response.
Does the health test fail to connect?
Return to the server terminal from earlier. Confirm the Task API is still running at http://localhost:3000.
Ask for help if the server is running but the request still fails: Help me diagnose why my Playwright health test cannot reach the local Task API.
- Add the valid task-creation test below test_health by pasting this code:
def test_create_task(api_request_context: APIRequestContext):
response = api_request_context.post(
"/tasks",
data={"title": "Learn API testing"},
)
assert response.status == 201
body = response.json()
assert body["title"] == "Learn API testing"
assert body["id"] >= 1
What does the creation test prove?
- The request sends the same valid title that you tested manually in Postman.
- The status assertion checks that the API reports successful task creation.
- The body assertions check the returned title plus the numeric identifier.
- Save tests/test_tasks_api.py.
- Run both positive contract tests by entering this command:
python -m pytest
You'll see both tests pass. The suite now protects the two API behaviors that already meet their contracts.
Does the creation test fail?
Check that the request path is /tasks. Confirm the payload title matches Learn API testing exactly.
Ask for help comparing the response if either assertion fails: Help me diagnose my Playwright valid task-creation test.
Preserve the defect as a failing test
Negative testing checks how an API handles rejected input. The empty title becomes a regression test for the expected validation rule.
- Add the empty-title test below test_create_task by pasting this code:
def test_rejects_empty_title(api_request_context: APIRequestContext):
response = api_request_context.post(
"/tasks",
data={"title": ""},
)
assert response.status == 400
assert response.json() == {"error": "title must be a non-empty string"}
What does the negative test preserve?
- The request reproduces the empty-title input from Postman.
- The first assertion encodes the expected 400 rejection status.
- The second assertion encodes the exact error body required by the contract.
- Save tests/test_tasks_api.py.
- Compare your cumulative test file using the tabs below.
✔️ Awesome, I've got everything!
Your fixture plus all three contract tests are in place.
ⓧ I'd like to double check the full code
import pytest
from playwright.sync_api import APIRequestContext, Playwright
BASE_URL = "http://localhost:3000"
@pytest.fixture(scope="session")
def api_request_context(playwright: Playwright):
request_context = playwright.request.new_context(base_url=BASE_URL)
yield request_context
request_context.dispose()
def test_health(api_request_context: APIRequestContext):
response = api_request_context.get("/health")
assert response.status == 200
assert response.json() == {"status": "ok"}
def test_create_task(api_request_context: APIRequestContext):
response = api_request_context.post(
"/tasks",
data={"title": "Learn API testing"},
)
assert response.status == 201
body = response.json()
assert body["title"] == "Learn API testing"
assert body["id"] >= 1
def test_rejects_empty_title(api_request_context: APIRequestContext):
response = api_request_context.post(
"/tasks",
data={"title": ""},
)
assert response.status == 400
assert response.json() == {"error": "title must be a non-empty string"}
Before you run the complete suite, do you think all three expectations match the API's current behavior?
- Run the complete suite against the naive API by entering this command:
python -m pytest
You'll see two tests pass plus one test fail. The failure shows that 201 did not equal the expected 400.
This failure is intentional. It preserves the missing validation rule as executable evidence.
Seeing a different test result?
- Return to the server terminal if every test reports a connection problem. Confirm the naive Task API remains running.
- Check that server.py still creates the task immediately after assigning title if the empty-title test passes.
- Confirm the second terminal still has .venv activated if Python cannot load the installed test packages.
Ask for help comparing your result with the intended failure: Help me diagnose why my naive API regression suite does not show two passing tests and one failing empty-title test.
You now have a red regression test that captures the defect precisely. Next, you'll add the validation guard that satisfies this contract.
Fix the API and Prove the Regression Suite
Your Playwright suite now proves that the Task API accepts an empty title with status 201, even though the contract expects 400.
The failing negative test points directly to the missing guard. Once the server restarts, the same regression suite proves the defect stays fixed.
In this step, get ready to:
- Add invalid-title handling before task creation.
- Restart the Task API with the saved server code.
- Prove all three contract tests pass.
Add the title validation guard
An API contract defines which inputs a service accepts. The guard must run before task is created so invalid data never reaches TASKS.
- In server.py, find the title = payload.get("title") if isinstance(payload, dict) else None assignment inside do_POST().
- Select from that assignment through self._send_json(201, task).
- Replace the selected block with this validated version:
title = payload.get("title") if isinstance(payload, dict) else None
if not isinstance(title, str) or not title.strip():
self._send_json(400, {"error": "title must be a non-empty string"})
return
task = {"id": len(TASKS) + 1, "title": title.strip()}
TASKS.append(task)
self._send_json(201, task)
What does this code do?
- The isinstance(title, str) check rejects values that are not strings.
- The title.strip() check rejects empty strings or strings containing only whitespace.
- The early return sends status 400 with the expected error JSON before task creation begins.
- The valid path stores the trimmed title before returning status 201.
- Save server.py.
- Confirm the if guard appears between the title assignment and the task dictionary.
Guard not in the right place?
- Check that the guard has eight leading spaces so it remains inside do_POST().
- Keep the task dictionary below the guard so invalid requests return before task creation.
Help me check the validation guard in my Task API.
✔️ Awesome, I've got everything!
Your server.py now rejects invalid titles before creating tasks.
ⓧ I'd like to double check the full code
import json
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
HOST = "localhost"
PORT = 3000
TASKS = []
class TaskHandler(BaseHTTPRequestHandler):
def _send_json(self, status_code, payload):
body = json.dumps(payload).encode("utf-8")
self.send_response(status_code)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def do_GET(self):
if self.path == "/health":
self._send_json(200, {"status": "ok"})
return
self._send_json(404, {"error": "not found"})
def do_POST(self):
if self.path != "/tasks":
self._send_json(404, {"error": "not found"})
return
content_length = int(self.headers.get("Content-Length", "0"))
raw_body = self.rfile.read(content_length)
try:
payload = json.loads(raw_body)
except (json.JSONDecodeError, UnicodeDecodeError):
self._send_json(400, {"error": "invalid JSON"})
return
title = payload.get("title") if isinstance(payload, dict) else None
if not isinstance(title, str) or not title.strip():
self._send_json(400, {"error": "title must be a non-empty string"})
return
task = {"id": len(TASKS) + 1, "title": title.strip()}
TASKS.append(task)
self._send_json(201, task)
if __name__ == "__main__":
server = ThreadingHTTPServer((HOST, PORT), TaskHandler)
print(f"Task API running at http://{HOST}:{PORT}")
server.serve_forever()
How to use this reference
Compare this reference with your saved server.py. The validation guard belongs after JSON parsing and before the task dictionary.
Restart the corrected API
Saving the file updates the code on disk. The running Python process still holds the old handler in memory.
- Switch back to the Command Prompt terminal running server.py from earlier.
- Press Ctrl+C to stop the old server.
The command prompt returns. This confirms the old process has released the local server port.
- Start the saved server.py code by running this command:
python server.py
What should I see?
You'll see a startup line containing http://localhost:3000. The terminal remains occupied while the corrected API listens for requests.
Server not restarting?
- Check that the earlier server process stopped before you restarted it.
- Check that the validation guard in server.py uses the same indentation as the surrounding request logic.
Help me troubleshoot why my corrected Task API will not restart.
Prove the regression suite
The same three tests now check both successful behavior and the repaired negative contract. No test code needs to change because it already describes the correct behavior.
- Switch back to the test terminal from the previous step.
Before the rerun, predict how the new guard changes the suite result.
- Rerun the Playwright suite by running this command:
python -m pytest
What should I see?
The pytest summary reports three passed tests. The empty-title test now receives status 400 while the two positive tests remain green.
Still seeing one failed test?
- Confirm the server terminal is running the saved version of server.py.
- Confirm the guard returns before the task dictionary is created.
- Confirm the test terminal still uses the activated .venv environment.
Help me diagnose why the empty-title regression test still receives status 201.
That's the loop closed: the fixed API now has a passing regression test protecting its contract.
Secret mission
Expand the Invalid-Input Matrix
One empty string cannot represent every invalid title. Expand your regression suite to cover missing, whitespace-only, and numeric titles through one reusable assertion helper.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your local lab running, pause it for later, or delete it completely. The Task API, Postman requests, and test suite stay on your computer with no ongoing costs.
Resources you used:
- The Task API process serving http://localhost:3000.
- The playwright-api-lab folder on your Desktop, including its .venv, API source, and tests.
Keep everything running
No action needed. Choose this if you are still testing the API or expanding the regression suite.
- Leave the Task API process running while you continue testing its endpoints.
- Keep the playwright-api-lab folder on your Desktop for future test runs.
Pause - I'll come back to this later
Stop the running process when you want the terminal back. This clears the temporary task list while keeping every project file.
- Switch back to Visual Studio Code.
- Select the terminal that is running the Task API.
- Press Ctrl+C to stop the server.
You'll see the Command Prompt return. The playwright-api-lab folder remains ready for another session.
Delete - I don't want to use this again
Deleting the lab is permanent, but its boundary is small. This option removes the Task API process and playwright-api-lab while keeping Python, Postman, and Visual Studio Code installed.
Stop the Task API process:
- Switch back to Visual Studio Code.
- Select the terminal that is running the Task API.
- Press Ctrl+C to stop the server.
- Close Visual Studio Code after the Command Prompt returns.
Delete the local project folder:
- Press Windows+E to open the Windows file browser.
- Navigate to the Desktop folder in the left sidebar.
- Select the playwright-api-lab folder.
- Press Shift+Delete to remove the folder permanently.
- Confirm the permanent deletion in the prompt.
You should no longer see playwright-api-lab on your Desktop. That clears the local API process, virtual environment, source code, and regression suite.
Nice Work!
Nice Work!
You made it. Your corrected local Task API now has a six-test regression suite protecting its title contract.
You've learned how to:
- Use Postman to inspect successful requests before reproducing the empty-title defect.
- Create a reusable Playwright APIRequestContext fixture for pytest contract tests.
- Fix the title-validation defect until all six regression tests pass.
- Complete the Secret Mission by expanding the suite across four invalid-title equivalence partitions.
Ready to quiz yourself?