The skill the job is actually made of. A repeatable method for tracing one request through an unfamiliar codebase, plus the pull request workflow and your capstone checkpoint.
Eleven modules built you something. This one turns that into employability, because the thing a team pays for is not your ability to start a project — it is your ability to become useful inside one that already exists. You will spend far more of your career reading code than writing it, and almost all of that code will be code you did not write and cannot ask the author about, because they left, or it was 2019, or the commit message says "fix".
Here is the wrong instinct, stated plainly, because almost everyone has it. You open the repository, expand the file tree, click the first file, and start reading from the top. Then the second file. You do this for two hours, you understand nothing, and you conclude you are not smart enough for this codebase.
You are not. The method is wrong. A codebase is a graph, not a book. A book is written to be read in order; nothing in a repository was. Files sit next to each other because of a folder convention, not because they run in sequence. The thing that actually has an order is a request — and the request cuts a path through fifteen files, in a specific order, ignoring the other four hundred. That path is the readable part. Everything else is scenery you can visit later, when a request takes you there.
Run these in order. Do not deviate, do not open an editor yet, and do not chase anything interesting until the checklist is finished — the value is in the sequence, because each step narrows where the next one looks.
# 1. What does this claim to be, and how do you run it?
cat README.md
# 2. What does it depend on? <- the single highest-signal file in the repo
cat requirements.txt # or pyproject.toml, or package.json
# 3. What is the shape? One level deep only. Do not recurse.
ls -la
ls app/ src/ 2>/dev/null
# 4. What actually starts?
cat Dockerfile | grep -i cmd
cat docker-compose.yml | grep -A2 command
grep -rn "FastAPI(" --include=*.py . # where is the app object created?
# 5. What is being worked on right now?
git log --oneline -20
# 6. Who knows this code?
git log --format=%an | sort | uniq -c | sort -rn | head
What each one actually tells you, because running them without knowing what you are looking for is just typing:
| Step | The question it answers | What to write down |
|---|---|---|
| README | What the authors think this is for, and the run command | The run command. Nothing else yet. |
| Dependencies | The architecture, before any source file | Web framework, DB layer, auth library, test runner |
| Tree, one level | How the authors divide responsibility | Folder names — they are the codebase's vocabulary |
| Entrypoint | The one function that runs first | File and line where the app object is built |
git log -20 | What is alive versus abandoned | Which directories appear in recent commits |
| Author counts | Who to ask, and where the risk is | Top three names; whether they still work there |
Step 2 deserves its own paragraph. Dependencies tell you the architecture before a single line of source does, because a library is a decision someone already made and cannot easily unmake. sqlalchemy plus alembic means an ORM with versioned migrations — so there is a models module and a migrations/ folder, guaranteed. celery plus redis means work happens outside the request, so some endpoints return before the work is done. python-jose or pyjwt means tokens, not sessions. You can predict two thirds of the folder tree from the manifest alone, and you should try to — then check, because where you are wrong is where this codebase is interesting.
The author count command reads oddly the first time. git log --format=%an prints one author name per commit, sort groups identical names together, uniq -c collapses each group to a count, and the second sort -rn orders by that count, largest first. It is the classic Unix pipeline shape and its output looks like this:
$ git log --format=%an | sort | uniq -c | sort -rn | head
312 Priya Raghavan
98 Tom Okafor
41 dependabot[bot]
12 Your Name
routers/, crud/, schemas/ is claiming those are three separate jobs. A repo with one 2,000-line main.py is claiming they are not. Both are real; know which you are in.package.json and you are looking for express, prisma, passport; step 4 greps for app.listen. On Rails, step 2 is the Gemfile, and step 4 is config/routes.rb — which hands you the entire route table in one file, a genuine luxury. The questions do not change. Only the filenames do.
Pick one request. Not a feature, not a module — one HTTP method and one path, with a concrete value in it. GET /places/1. The concreteness matters: "the places endpoints" is a category and categories cannot be traced, but GET /places/1 is a single path through the graph with a beginning and an end.
grep -rn "places" --include=*.py .. You are looking for a decorator, a route registration, or a router include — something that ties the string /places to a function.path/to/file.py:line. This is step zero of your trace and everything else hangs off it.Depends(...) argument is a function call that already ran before your handler started, and beginners miss those every time.Here is the method applied to a request you already own. Run it against your own City Guide API first — you can check every claim below against your actual files, which is exactly why you should practise here before applying it to a stranger's repository. Your line numbers will differ; the shape should not.
Two things in that diagram catch every beginner. First, steps 5 and 6 happen before your handler body runs — the database session and the authenticated user already exist by the time your first line executes, which is why a broken token produces a 401 from code you never wrote. Second, step 14 happens after your return statement. Code running after a return is not intuitive, and it is where connection-leak bugs live in every framework ever written.
git log -p for history, git blame for context, the test suite for intent, the debugger for truth.# LOCATE - where does this string appear at all?
grep -rn "places" --include=*.py . # -r recursive, -n line numbers
grep -rn "def get_place" --include=*.py . # find a definition
grep -rn "get_place(" --include=*.py . # find the callers - note the paren
# HISTORY - how did this file become what it is?
git log -p app/routers/places.py # every change to this file, with diffs
git log --oneline app/routers/places.py # ... just the messages, for a first pass
# CONTEXT - what commit introduced this confusing line, and why?
git blame app/routers/places.py # every line, with its commit and author
git show a1b2c3d # the full commit that line came from
# INTENT - what did the authors believe should happen?
pytest tests/ -q # do they even pass?
cat tests/test_places.py # an executable description of the API
# TRUTH - what actually happens, right now, with real values?
# put breakpoint() on the first line of the handler, then hit the endpoint
The paren in grep -rn "get_place(" is the whole trick. Without it you get the definition, every import, and every mention in a comment. With it you get call sites, which is what you actually wanted. Learning to grep for the shape of a call rather than a bare name is worth more than any IDE feature.
git blame is for context, never for blame. The name is a historical joke that has done real damage. What it gives you is a line-to-commit mapping, and the commit gives you a message, a date, and usually an issue reference — which is to say, the reason a strange line exists. Nine times out of ten the strange line is strange because it fixes something, and the commit message tells you what. Run git show on the hash and read the message before you touch the line.
The test suite is documentation that cannot go stale, because a stale test fails. tests/test_places.py is an executable description of what the authors believed the code should do — including edge cases they hit in production and never wrote down anywhere else. When a README and a test disagree, the test is right. Read the test names first: a list of test names is a specification, and you can read forty of them in three minutes.
And when reading fails, stop reading. Put breakpoint() at the top of the handler, send the request, and look at the actual values:
# in the handler, temporarily:
def get_place(place_id: int, db: Session = Depends(get_db)):
breakpoint() # execution stops here, terminal becomes a prompt
...
# then in the terminal running your server:
(Pdb) place_id # what value actually arrived?
1
(Pdb) type(place_id)
<class 'int'>
(Pdb) db # what IS this thing?
<sqlalchemy.orm.session.Session object at 0x104a2b1d0>
(Pdb) w # 'where' - print the full call stack. THE trace, for free
(Pdb) n # next line
(Pdb) s # step INTO the next call
(Pdb) c # continue, let the request finish
That w command prints the entire chain of calls that led to this line — the machine handing you a large part of your trace, with real line numbers, in one keystroke. Thirty seconds in a debugger routinely beats an hour of reading, and beginners avoid it purely because it is unfamiliar. Get familiar.
breakpoint() before committing. One left in a handler hangs the server for whoever runs it next, with no error message. Grep for it as part of your pre-commit habit.git log -p on a confusing file is a story. Read it newest-first and stop when it starts making sense. You are looking for the commit where the current shape appeared.notes/request-trace.md in your own repository, alongside your module notes.# Request trace: GET /places/1
Repo: city-guide (my own) Commit: a1b2c3d Date: 2026-03-14
## Path
1. app/main.py:14 FastAPI() app object created
2. app/main.py:26 include_router(places.router)
3. app/middleware.py:22 request id attached, timer started
4. app/routers/places.py:31 @router.get("/places/{place_id}")
5. app/db.py:18 get_db() dependency - opens Session
6. app/auth.py:44 current_user() - decodes JWT, loads User
7. app/routers/places.py:33 handler get_place() body starts
8. app/crud/places.py:12 db.get(Place, place_id)
9. app/models.py:9 Place ORM instance returned (or None)
10. app/schemas.py:27 PlaceRead validates and drops internal fields
11. app/db.py:21 finally: db.close() -- runs AFTER the return
## Transformations
- "1" in the URL path -> int, by the type hint on the handler param
- primary key int -> SELECT ... WHERE places.id = 1
- row -> Place ORM instance
- Place instance -> PlaceRead (owner_id dropped, tags flattened)
- PlaceRead -> JSON bytes, Content-Type application/json
## Three things I cannot yet explain
1. Who calls get_db()? I never call it - FastAPI does, but where?
2. If two requests arrive at once, do they share a Session? What stops them?
3. tests/conftest.py:31 overrides get_db - how does the real one get replaced?
The three questions are the most valuable part of that file, and it is worth being precise about why. A trace is a thing you can reconstruct any time in twenty minutes. A precise list of what you do not understand is not reconstructible, because it is a snapshot of the edge of your knowledge and it moves.
More practically: knowing exactly what you do not understand is the difference between a junior who is stuck and a junior who is useful. Compare two messages to a senior engineer. "I don't get the database stuff" is unanswerable — it demands an hour they do not have and gives them no idea where to start. "I traced GET /places/1. I can see get_db is declared as a dependency at routers/places.py:33, but I cannot find who calls it, and I do not understand how conftest.py:31 replaces it in tests" takes ninety seconds to answer and makes you look like someone worth answering. That second message is the trace file, quoted.
git switch -c, commit, push to a remote, open the request, respond to comments, merge.Module 00 gave you six git commands and promised branching would come when you had someone to merge with. This is that module. You may still not have a person — so you are going to be both sides of the loop, which is a genuinely useful exercise because it forces you to read your own diff as a stranger would.
Why work happens on a branch. The main branch is the version that is believed to work. If you commit half-finished work onto it, everyone who pulls gets your half-finished work, and there is no way to say "not that bit". A branch is a private line of history with a name, and it lets a change be complete before anyone else has to care about it. It also makes the change reviewable as a unit, which is the thing a pull request actually is: a diff, plus an argument for why it should exist.
# start work
git switch -c trace/places-notes # create the branch and move onto it
git status # confirm: "On branch trace/places-notes"
# ... make your change, then ...
git add notes/request-trace.md
git commit -m "add request trace for GET /places/1"
# publish it
git push -u origin trace/places-notes # -u links local branch to remote branch
# after the review, back on main
git switch main
git merge trace/places-notes
git branch -d trace/places-notes # delete the merged branch
No remote? Make one. A "remote" is just another git repository that yours knows about, and it can be a folder on your own disk. A bare repository is one with history but no working files — exactly what a server holds. This gives you the complete push-and-pull mechanic with no account, no network, and nothing to sign up for:
cd ~/projects
git clone --bare city-guide city-guide-origin.git # a local "server"
cd city-guide
git remote add origin ~/projects/city-guide-origin.git
git remote -v # confirm it is registered
git push -u origin trace/places-notes # a real push, entirely offline
What makes a pull request reviewable. Four things, and they are all about respecting the reviewer's attention:
Responding to review comments has two failure modes, and beginners hit both. Capitulating — changing the code because someone senior asked, without understanding why — leaves you unable to defend or maintain it, and quietly teaches you nothing. Getting defensive treats the comment as a judgement of you rather than of a diff. The middle path is a question: "I did it that way because X — does that hold, or am I missing something?" That sentence resolves most review disagreements in one round, because it surfaces the reasoning that the diff could not.
Reviewing someone else's PR has an order too. Read the description first, so you know what the change is trying to do. Then read the tests, because they tell you what the author believed "working" means — and a change with no test is itself a comment worth making. Only then read the diff, and read it against the intent you now hold.
On a real team this would be a live walkthrough with a mentor, and the value would come from having to say things out loud to a person who can ask "why?". You do not have that person, so the substitute is a recording — and it is a better substitute than it sounds, because hesitation is audible. The place where you go quiet, or say "and then it sort of... handles it", is exactly the part you do not understand. You will not notice that while talking. You will notice it immediately on playback.
curl or the interactive docs, narrating as you go.GET /places/1, from socket to JSON. This is the single item that matters most, and it is the one interviewers actually ask.notes/request-trace.md, and say what you would do to answer each one.What is not being assessed: elegance, how much of the traced codebase you read, whether your schema diagram is neat, how many endpoints exist, test coverage percentage, or whether your folder structure matches anyone's blog post. None of that is on the list, deliberately.
What is being assessed: whether it runs from a clean clone, and whether you can explain why it runs. That is the entire bar, and it is the same bar a first interview applies.
docker compose up" and omits that you must first copy .env.example to .env, because on your machine that file has existed since Module 00. A clean clone has no .env. Everything your README assumes but does not state is a thing that works only on your laptop — which is precisely the failure mode Module 11 spent seven hours eliminating from your container. Do not let it back in through the documentation.
The frontend half of this track follows the same shape: HTML, CSS and the DOM as three separate ideas; the browser runtime and its event loop; components and state; then the module that joins the halves — your own page calling your own API, which means CORS, token storage, and the security questions that come with putting a JWT anywhere a browser can reach. Then forms validated on both sides (client for speed, server for truth — never only one), and a build and deploy step.
What this backend track deliberately did not cover, named so you know they exist and know you have not learned them. Do not go and study these now; go and hit a problem that needs one.
| Topic | The problem it solves |
|---|---|
| Background jobs and queues | Work too slow to do inside a request — emails, imports, image processing |
| Caching | The same expensive answer computed a thousand times a second |
| WebSockets | The server needing to speak first, rather than only answering |
| Rate limiting | One client, deliberately or accidentally, consuming the service |
| CI | Running your tests automatically on every push, so nobody has to remember |
Each of those is a module's worth of work and each one attaches to the project you already have. That is the last piece of advice this track has: keep extending the City Guide API. A project you have run for months teaches you things a fresh tutorial cannot, because only an old project has history, forgotten decisions, and code you have to trace — which is where this module started.
notes/request-trace.md with line numbers and three unanswered questions, a branch-and-pull-request cycle practised end to end (against a local bare repo if you have no remote), and the capstone checkpoint completed — recorded, watched back, and its hesitations written down.
notes/request-trace.md with the endpoint, the line-numbered path, the transformations, and three questions you cannot answer.git checkout -- <file>.--bare clone), then merge it back and delete the branch.notes/module-12.md.Every check below is objectively pass or fail from your own terminal. Run them in the repository you traced, then in your own.
$ git log --format=%an | sort | uniq -c | sort -rn | head -3
312 Priya Raghavan # counts, largest first, one line per author
98 Tom Okafor # any names - the SHAPE is what you are checking
41 dependabot[bot]
$ grep -rn "places" --include=*.py . | head -5
./app/routers/places.py:31:@router.get("/places/{place_id}") # file:line:match
./app/main.py:26:app.include_router(places.router) # non-empty = grep works
$ test -f notes/request-trace.md && echo FOUND || echo MISSING
FOUND # the trace file exists
$ grep -cE "\.py:[0-9]+" notes/request-trace.md
11 # must be 5 or more: line-numbered steps
$ grep -c "^[0-9]\." notes/request-trace.md
3 # the three numbered unanswered questions
# (0 means you skipped the most valuable part)
# --- the README test: fresh clone, own README only, timed ---
$ mkdir -p /tmp/readme-test && cd /tmp/readme-test
$ date +%s > .start
$ git clone ~/projects/city-guide app
Cloning into 'app'...
done. # a local path clones fine - no network needed
$ cd app
# ... now follow ONLY your README, typing nothing from memory ...
$ echo $(( $(date +%s) - $(cat ../.start) )) seconds
412 seconds # under 600 = pass. Over = your README is wrong.
# --- the branch cycle ---
$ git remote -v
origin /Users/you/projects/city-guide-origin.git (fetch)
origin /Users/you/projects/city-guide-origin.git (push)
$ git branch --merged main
* main
trace/places-notes # listed here = it was merged. Absent = it wasn't.
$ git log --oneline -1
a1b2c3d add request trace for GET /places/1
grep -c "^[0-9]\." returns 0, the module is not finished. A trace with no questions means one of two things: you traced something you already fully understood, or you wrote plausible guesses instead of admitting gaps. Both are worth going back for. The questions are the deliverable; the path is just how you found them.
Answer each in writing, in your own words, in notes/module-12.md, before opening the answers. This is the last set in the track — take it slowly.
1. Where do the structural choices of the codebase you traced differ from yours, and why might that be?
Your answer is specific to what you traced, so judge it on whether you reached for reasons rather than verdicts. Real ones: age (a codebase from before FastAPI's dependency system will pass a session around by hand), scale (a repo with fifty endpoints splits crud/ out; one with four does not, and splitting early is over-engineering), team size (more authors means more layers, because layers are how people avoid editing the same file), and constraints you cannot see — a strange abstraction is often a scar from an incident. "This is worse than mine" is almost always a failure to find the constraint. Assume a reason exists and go looking for it in git log.
2. What are your three unanswered questions, and why is that list more valuable than the trace itself?
The trace is reproducible — you can rebuild it in twenty minutes with grep. The questions are not, because they are a snapshot of the boundary of your knowledge, and that boundary moves and then you cannot remember where it was. Practically, the list is what makes you cheap to help: a senior can answer three file-and-line questions in five minutes, but cannot answer "I don't get it" at all. It is also the honest measure of progress — questions that get more specific over time mean you are learning, even on weeks where nothing you wrote worked.
3. What makes a pull request easy to review?
One concern, small enough to hold in your head, a description saying what and why (the diff already says how), and passing tests before you ask. The underlying reason is that review is a scarce, expensive attention budget: a 900-line PR does not get 900 lines of scrutiny, it gets "LGTM" and false confidence, which is worse than no review because everyone now believes it was checked. Splitting a change into three PRs is not more work for the reviewer — it is less, because each one can be understood alone. The refactor and the fix should never be the same PR.
4. Why does reading a test suite tell you more than reading the README?
Because a stale test fails and a stale README does not. A README is a claim written once, usually at the start, and nothing forces it to stay true. A test is executed on every run, so if the code drifts away from it, someone must either fix the code or change the test — either way the test tracks reality. Tests also record what a README never does: the edge cases, the error codes, the exact shapes. Read the test names first — forty names is a specification you can absorb in three minutes. And a behaviour with no test is a behaviour nobody has promised to keep.
5. Which single part of your own City Guide API would you now build differently, and what would you change?
Any specific answer with a reason attached is a good one. Common honest ones: the schema, because a question arrived that the tables made awkward — which is exactly the Module 04 argument that queries drive the model. Error handling scattered across handlers instead of centralised. Auth checks written per-endpoint rather than as one dependency. Or tests that assert on internals and now break whenever you refactor. Note what the answer is not: "I'd use a different framework". Framework choice is rarely the thing that hurt you. What hurts is a decision made before you knew the question it was answering, which is unavoidable and is why the second version of anything is better.
6. You change one line you believe is unimportant and the test suite fails in three unrelated files. What have you learned?
That the line was load-bearing and you could not see the load — which is the actual finding, not the failure. Three unrelated failures means hidden coupling: something reaches into that value from far away, probably through shared state, a default that everything relies on, or an implicit ordering. Read the three failing tests before fixing anything; together they describe the dependency the code does not document. This is also the most efficient probe you have in an unfamiliar system. Reading tells you what the code says; deliberately breaking it tells you what the code holds up, and those are different maps.