Standalone lab
Build an API With GitHub Copilot Agent Mode
Use agent mode to build a small FastAPI service across multiple files, then review what it actually changed — including the two things it will get wrong unless you tell it not to.
- Intermediate
- Medium lab
- 4 min read
What you will be able to do
- Drive a multi-file change with agent mode from a single well-scoped request
- Establish a baseline before the agent runs, so a failure afterwards is attributable
- Review an agent diff by file rather than by accepting the summary
- Identify the validation and error-handling gaps agent mode leaves by default
- Iterate with a follow-up prompt instead of restarting
Before you start
- Python 3.11 or later
- An editor with agent mode — VS Code, Visual Studio, or a JetBrains IDE
- A Copilot plan that includes agent mode (Free does not)
- Git, so you can read a diff
Preparation from the Academy: GitHub Copilot Agent Mode Explained, GitHub Copilot Agent Mode in VS Code, GitHub Copilot for Python
Agent mode is the first Copilot feature where you stop reviewing a suggestion and start reviewing a change set. That is a different skill, and this lab is about that skill rather than about FastAPI.
You will build a small inventory API in one agent run, then spend most of the lab reading what it did.
Step 1 — Baseline first
mkdir inventory-api && cd inventory-api
git init
python -m venv .venv && source .venv/bin/activate
pip install "fastapi>=0.110" "uvicorn[standard]" pytest httpx
printf '.venv/\n__pycache__/\n' > .gitignore
git add -A && git commit -m "empty project"Step 2 — Constrain it before you run it
.github/copilot-instructions.md:
# Copilot instructions
A small FastAPI inventory service. Python 3.11+.
## Structure
- `app/models.py` — Pydantic v2 models only. No logic.
- `app/store.py` — in-memory storage. Pure functions or a simple class.
- `app/main.py` — routing only. Endpoints delegate immediately.
- `tests/` — pytest.
## Rules
- Every request model constrains its fields: min/max length or a pattern on
strings, bounds on numbers. An unbounded string on a public endpoint is a
denial-of-service surface.
- Allocation is all-or-nothing. Never partially fulfil a request.
- Return 409 for a conflict with current state, 422 for a malformed request.
Never return 200 with an error in the body.
- Type-annotate every signature.
- No database, no auth, no logging middleware.Step 3 — One request, well scoped
Open agent mode and give it the whole task at once:
Build the inventory service described in the instruction file. It needs: POST /items to create an item with a SKU, name and quantity; GET /items/:sku to fetch one; POST /items/:sku/allocate to allocate a quantity, failing with 409 if there is not enough stock; and GET /health. Include tests for the allocation success and failure paths.
Step 4 — Review the change set, not the summary
The agent will report what it did. Do not read that. Read this:
git status --short
git diff --statThen go file by file:
git diff -- app/models.py
git diff -- app/store.py
git diff -- app/main.py
git diff -- tests/Three questions per file, in this order:
- Does it do what the instruction file said? Bounded fields, all-or-nothing allocation, 409 versus 422.
- Did it touch anything you did not ask about —
.gitignore, a config file, a dependency? - Is there an error path with no test?
In our runs, agent mode reliably produced working endpoints, correct status codes for the happy paths, and reliably missed at least one of:
- a negative or zero quantity on allocate, which passes validation and silently increases stock
- allocating against an unknown SKU, returning 500 rather than 404
- a duplicate SKU on create, which overwrites the existing item
Find which one applies to your run before reading on.
Step 5 — Iterate, do not restart
The instinct when an agent leaves a gap is to rewrite the prompt and run it again. Don’t. Follow up:
Allocate accepts a quantity of zero or less and increases stock. Add a bound to the request model so it cannot, add a test that fails without the bound, and change nothing else.
“Change nothing else” is doing real work in that sentence. Without it, a
follow-up frequently reformats files it had no reason to touch, and your next
git diff is unreadable.
Then diff again:
git diff --statIf that shows more than the two files you expected, ask why before accepting.
Validation
1. The service starts and answers:
uvicorn app.main:app --port 8000 &
sleep 2
curl -s -X POST localhost:8000/items \
-H 'content-type: application/json' \
-d '{"sku":"WIDGET-1","name":"Widget","quantity":10}'
curl -s localhost:8000/items/WIDGET-12. Allocation is all-or-nothing:
# Succeeds, leaving 7
curl -s -X POST localhost:8000/items/WIDGET-1/allocate \
-H 'content-type: application/json' -d '{"quantity":3}'
# Must fail with 409 and must NOT partially allocate
curl -s -o /dev/null -w '%{http_code}\n' -X POST localhost:8000/items/WIDGET-1/allocate \
-H 'content-type: application/json' -d '{"quantity":99}'
# Still 7 if the failure was clean
curl -s localhost:8000/items/WIDGET-13. The gaps are closed:
# zero quantity -> 422, not 200
curl -s -o /dev/null -w 'zero: %{http_code}\n' -X POST localhost:8000/items/WIDGET-1/allocate \
-H 'content-type: application/json' -d '{"quantity":0}'
# unknown sku -> 404, not 500
curl -s -o /dev/null -w 'unknown: %{http_code}\n' -X POST localhost:8000/items/NOPE/allocate \
-H 'content-type: application/json' -d '{"quantity":1}'4. The tests pass:
pytest -qStop the server when you are done:
kill %1Troubleshooting
Agent mode is not in the model picker. It is not available on Copilot Free. Check your plan on the plans comparison.
The agent edited files outside the project. Close the parent folder and open the project directory itself as the workspace root. The agent’s scope is the workspace, and an over-broad root is the usual cause.
git diff shows the whole file as changed. Line endings. Commit a
.gitattributes with * text=auto before the next run.
The agent’s summary and the diff disagree. Trust the diff. The summary is generated from what the model intended; the diff is what happened.
Security considerations
- The service has no authentication and binds to localhost. Do not expose it.
- The instruction file’s bounded-field rule is a security rule, not a style one: an unbounded string field on a public endpoint is a memory-exhaustion vector.
- Never paste a real API key into a prompt to “make the example concrete”. Use
export API_TOKEN="YOUR_TOKEN_HERE"and keep secrets in.env, gitignored.
Cleanup
Nothing was provisioned. To remove the project:
kill %1 2>/dev/null
deactivate
cd .. && rm -rf inventory-apiWhat you should take away
The agent wrote the code in one pass. The value you added was the baseline
commit, the instruction file, and four git diff commands — none of which are
about Copilot, and all of which are why you can tell whether the result is right.
The failure mode to remember: an agent’s summary describes its intent. Only the diff describes the change.
Sources
Every version-sensitive claim on this page was checked against first-party documentation. Only sources actually used are listed.
Primary sources
Finished the lab?
Stored in this browser. It appears on your progress dashboard, and syncs across devices if you sign in.
Was this lesson helpful?
We record which lesson you rated and whether it helped. Nothing identifies you — no account, no cookie, no session.