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.
Checkpoints
Five checkpoints, one per stage of the lab. Ticks are kept in this browser; the two markedbrowser-checked test the output you paste against what the command should print — in the page, sent nowhere.
of 5 checkpoints
- 1. Understand the taskSelf-reported
Establish the baseline and constrain the agent
Steps 1 and 2: know what the project looks like before the agent touches it, and write the rules it must follow. Go to the section
- 2. Make the changeSelf-reported
Make one well-scoped request and review the change set
Steps 3 and 4. Read the diff, not the summary. List every file the agent changed and one thing in each you would not have written that way. Go to the section
- 3. Run and checkBrowser-checked
Prove allocation is all-or-nothing
Run validation block 2. The over-allocation must return 409 and the stock must still read 7. Paste the status line. Go to the section
The pasted text must contain the status code 409 — what curl prints for the request that must fail.
- 4. Interpret the resultBrowser-checked
Close the gaps and check the codes
Validation block 3: zero quantity must be 422, unknown SKU 404. Paste both lines. Go to the section
The pasted text must contain "zero: 422" and "unknown: 404", as the two curl commands print them.
- 5. ContinueSelf-reported
Run the tests and decide what to merge
pytest must pass. Then decide: what in the agent's change set needs a human to look again before it ships? Write the list. Go to the section
What you have now
A FastAPI service built by agent mode from a scoped request, with the change set reviewed line by line, atomic allocation proven by a failing request that changed nothing, and the gaps the agent left closed and tested.
Where to go next
- Lesson What to look for in an agent-authored change
- Lab Build an MCP integration
- Tool Generate the instruction file this lab had you write, for your own project
Then mark the lab complete below if you want it on your My Academy record.
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.