Module 00 · ~6 hours · Priority: High

Setup & Terminal Literacy

The hour-one setup nobody writes down: a terminal you can drive, git that isn't scary, Python isolated per project, Postgres in a container, secrets in a file you never commit — and how to read an error message instead of pasting it somewhere.

How to use this track read once

This is a build track, not a reading track. Every module ends with something running on your machine that did not exist that morning.

What
Thirteen modules that grow one project — a City Guide API — from a single endpoint to a containerised service with auth, tests, and logs.
Where
Entirely on your own machine. No cloud account, no credit card, no signup for anything. Docker is the only large install.
When
Start here even if you have written code before. Module 00 is the setup that every other tutorial assumes you already did.
How
Read the module, build the thing, run the verify commands, then answer the Prove it questions in your own words before opening the answers.

Five rules. They matter more than any individual page here.

  • 45-minute reading cap per module. Set a timer. When it ends, build with what you have, even if you feel underprepared. Feeling underprepared is the normal working state of a backend engineer, not a signal to read more.
  • Nothing is done until it runs and you can explain it. If you cannot trace what happens between the request arriving and your function running, the module is not finished — regardless of whether the code works.
  • No unmodified AI output. Use AI freely, but every generated block must be run, then deliberately changed: rename something, delete a line, break it on purpose, read the error. If you cannot break it, you do not understand it.
  • Stuck for 30 minutes? Write the question down before you ask anyone. One sentence: what you expected, what happened, what you already tried. Half the time writing it solves it. The other half, it makes the answer three times faster.
  • Commit at the end of every session, even a session that went badly. The commit message says what you learned, not what you typed.
The hour budgets are hours, not days. "~6 hours" means six hours of focused work, whether that is one Saturday or six weeknights. If a module takes you twice as long, that is information about the module's difficulty, not about you.
Do not follow a video build-along for these modules. You will end the day with someone else's project on your disk and none of it in your head. Use videos to understand a concept you are stuck on, then close the tab and type your own version.

What this track deliberately does not do — it does not teach you every feature of any framework, it does not make your code elegant, and it does not cover frontend yet. The frontend half is on the track index as a published roadmap. What it does do is get you to the point where you can read a backend codebase you have never seen and follow what happens.

The terminal

What
A text interface to your computer. You type a command, it runs a program, the program prints its result.
Where
Terminal.app or iTerm on macOS, Windows Terminal running WSL on Windows, any terminal emulator on Linux.
When
Constantly, from here on. Servers have no windows to click. Every tool in this track is a command.
How
Learn roughly ten commands. You will use those ten for 95% of everything, and look up the rest when you need them.

You do not need to be fast at this. You need to stop being afraid of it. A terminal cannot break your computer in normal use, and every command below only reads or creates — none of them delete anything.

pwd                       # print working directory - "where am I?"
ls                        # list files here
ls -la                    # ... including hidden ones (dotfiles), with details
cd projects               # move into the projects folder
cd ..                     # move up one level
cd ~                      # go to your home folder
mkdir city-guide          # make a new folder
cat README.md             # print a whole file
less README.md            # page through a long file (press q to quit)
grep -rn "database" .     # find "database" in every file below here, with line numbers
curl http://localhost:8000/health   # make an HTTP request from the terminal
which python3             # where does this command actually live?

Two symbols do most of the heavy lifting. The pipe | feeds one command's output into the next, and > writes output into a file instead of the screen:

ls -la | grep ".py"              # only the Python files
cat app.log | grep "ERROR"       # only the error lines
cat app.log | grep "ERROR" | wc -l   # ... and count them
echo "SECRET=abc" > .env         # write into a file (overwrites!)
echo "DEBUG=1" >> .env           # append to a file (safer)
  • Tab completes. Type cd city then press Tab. Half of terminal speed is just Tab.
  • Up arrow repeats. Your last hundred commands are one arrow key away. Ctrl+R searches them.
  • Ctrl+C stops a running program. You will need this every time you run a server. It is not a crash; it is the normal way to stop.
  • A command that prints nothing usually succeeded. Unix tools are silent on success and loud on failure. Silence is good news.
  • The path matters. Most "it doesn't work" moments in your first week are you running a command in the wrong folder. Type pwd before you panic.
Windows: use WSL, not PowerShell. Install Windows Subsystem for Linux and do all your work inside it. Every backend instruction you will ever read assumes a Unix-style shell, and translating them yourself while also learning the concept is two problems at once.

Git, the useful minimum

What
A tool that records snapshots of your project over time, so you can see what changed, undo it, and work alongside other people.
Where
Every professional codebase, without exception. Your project gets one on day zero.
When
Commit at every point where the project works. "Works" is the unit, not "finished".
How
Six commands cover your first month. Branching and merging come in Module 12, when you actually have someone to merge with.
git init                      # start tracking this folder
git status                    # what has changed? RUN THIS CONSTANTLY
git add .                     # stage everything changed
git add app/main.py           # ... or stage one file
git commit -m "add places endpoint"   # save a snapshot with a message
git log --oneline             # list past snapshots, newest first
git diff                      # what did I change since the last commit?

Staging is the part that confuses everyone. Git has three places a change can live: your working folder (you edited a file), the staging area (you ran git add), and history (you ran git commit). The staging area exists so you can commit some of your changes and not others.

edit file git add git commit | | | v v v working folder --> staging area --> history (permanent) ^ | |_______________ git checkout ________|

Before your first commit, create a .gitignore file. This tells git which files to never record — and it is the single most important file in this module, because it is what stops you committing a password:

# .gitignore
.env                 # secrets - NEVER commit this
__pycache__/         # Python's compiled cache
*.pyc
.venv/               # your virtual environment, rebuildable from requirements.txt
.DS_Store            # macOS folder metadata
*.db                 # local database files
  • Commit messages are for the future you who is trying to work out when something broke. "fix" tells that person nothing. "reject empty place names with 422" tells them everything.
  • Commit small and often. A commit that touches forty files cannot be understood or undone. A commit that adds one endpoint can be both.
  • A committed secret is a leaked secret, even if you delete it in the next commit — git history keeps it. This is why .gitignore comes before your first commit, not after.
  • You do not need GitHub to use git. Everything above works entirely offline on your own machine. Pushing to a remote is a separate, later thing.

Python and virtual environments

What
A virtual environment is a private folder of libraries belonging to one project, instead of one shared pile belonging to your whole computer.
Where
One per project, in a .venv folder next to your code, never committed to git.
When
Created before you install a single library. Activated every time you open a new terminal to work on the project.
How
Built into Python: python3 -m venv .venv, then activate it, then install into it.

Why this exists. Project A needs version 1 of a library. Project B needs version 3. If both install into your system Python, one of them breaks — and the breakage looks like a mysterious error in code you did not touch. A virtual environment gives each project its own isolated shelf.

python3 --version              # need 3.11 or newer for this track

mkdir city-guide && cd city-guide
python3 -m venv .venv           # create the environment
source .venv/bin/activate       # activate it (macOS / Linux / WSL)
                                # your prompt now shows (.venv)

pip install fastapi uvicorn     # installs INTO .venv, not your system
pip freeze > requirements.txt   # record exact versions so others can rebuild it

deactivate                      # leave the environment

The (.venv) prefix on your prompt is how you know it is active. If it is missing, pip install is polluting your system Python and python is running the wrong interpreter — which produces the single most common beginner error message in the world:

ModuleNotFoundError: No module named 'fastapi'

You installed it. It is genuinely there. You are just not standing in the environment where it lives. Run source .venv/bin/activate and try again.

  • Never commit .venv/. It is hundreds of megabytes and machine-specific. requirements.txt is the thing you commit — it is the recipe, not the meal.
  • Activate per terminal window. Opening a new tab means activating again. Nothing is broken; environments are per-shell.
  • Pin your versions. pip freeze writes fastapi==0.115.0, not fastapi. A project that installs "whatever is newest" breaks on a Tuesday for no reason.
In other stacks: Node solves the same problem with a per-project node_modules/ folder and package.json, which is why it needs no activation step. Ruby uses Bundler, Java uses Maven or Gradle. Same idea everywhere: dependencies belong to a project, not to a computer.

Postgres in a container

What
Docker runs a program with its own filesystem and dependencies bundled in, so installing Postgres becomes one command instead of an afternoon.
Where
Your machine now; the same image runs unchanged on a server later. That portability is the entire point of containers.
When
From this module onward. You will not install Postgres natively at any point in this track.
How
Install Docker Desktop, write a five-line docker-compose.yml, run docker compose up -d.

You could install Postgres directly. Do not. Native installs differ per operating system, leave background services running you forget about, and are painful to reset when you inevitably corrupt your practice data. A container is disposable, which is exactly what you want while learning.

# docker-compose.yml
services:
  db:
    image: postgres:16
    environment:
      POSTGRES_USER: cityguide
      POSTGRES_PASSWORD: localdevpassword
      POSTGRES_DB: cityguide
    ports:
      - "5432:5432"          # host port : container port
    volumes:
      - pgdata:/var/lib/postgresql/data   # keep data between restarts

volumes:
  pgdata:
docker compose up -d          # start it in the background
docker compose ps             # is it running?
docker compose logs db        # what did it print?
docker compose down           # stop it (data survives, it is in the volume)
docker compose down -v        # stop AND delete the data - your reset button

# open a SQL prompt inside the running container
docker compose exec db psql -U cityguide -d cityguide

Read that ports line carefully, because it is the source of half of all container confusion. "5432:5432" means "traffic arriving at port 5432 on my laptop should be forwarded to port 5432 inside the container". The container is a separate little machine with its own network. Nothing inside it is reachable unless you publish a port.

your laptop container ------------ --------- psql / your app ---> :5432 --> :5432 postgres (published)
  • -d means detached — it runs in the background and gives you your prompt back. Without it, the logs stream into your terminal and Ctrl+C stops the database.
  • The volume is why your data survives. Delete the container and the data lives on in pgdata. That is also why down -v is the real reset — it removes the volume too.
  • "port is already allocated" means something else is on 5432, usually a Postgres you installed and forgot. Change the left number to "5433:5432" and connect to 5433 instead.
  • You are not learning Docker yet. You are using it. Module 11 is where you build your own image and understand what these lines mean in full.
That password is fine here and nowhere else. localdevpassword lives on your laptop, on a port only your machine can reach. The moment anything is reachable from outside your machine, this becomes a real credential and belongs in the environment, not in a committed file. Module 10 covers doing this properly.

Config and secrets, from hour one

What
Anything that changes between your laptop and a real server — database URLs, ports, keys, passwords — read from the environment rather than typed into the code.
Where
A .env file locally (git-ignored), real environment variables everywhere else.
When
Now. Retrofitting this later means hunting hardcoded strings through a codebase, and missing one.
How
Keep a committed .env.example listing every key with fake values, and an ignored .env holding the real ones.
# .env          <- git-ignored, real values, never committed
DATABASE_URL=postgresql://cityguide:localdevpassword@localhost:5432/cityguide
JWT_SECRET=dev-only-not-a-real-secret
LOG_LEVEL=debug

# .env.example  <- committed, documents what is needed
DATABASE_URL=postgresql://user:password@localhost:5432/dbname
JWT_SECRET=
LOG_LEVEL=debug

The pair matters. .env.example is how the next person — including you on a new laptop — knows which values the project needs without ever seeing a real one. It is documentation that cannot go stale, because the app fails immediately if a key is missing.

  • The rule is one line long: if it differs between your machine and production, or if it would be bad for a stranger to read it, it is config — not source.
  • Check git status before every commit in your first weeks. If .env appears in that list, your .gitignore is wrong. Fix it before committing, not after.
  • Environment variables are strings. PORT=8000 arrives in your program as the text "8000". Converting and validating them is real work, which is why Module 10 does it with a settings object rather than scattered os.getenv calls.

Reading an error instead of pasting it

What
A traceback is the list of function calls that were in progress when your program failed, printed innermost-last.
Where
Your terminal, your server logs, your test output. You will read thousands of these.
When
Every single failure. Before searching, before asking, before pasting it into a chat window.
How
Read the last line first, then walk upward to the last line that names a file you wrote.
Traceback (most recent call last):
  File "/app/.venv/lib/python3.12/site-packages/uvicorn/protocols/http.py", line 435, in run_asgi
    result = await app(self.scope, self.receive, self.send)
  File "/app/main.py", line 42, in get_place
    return places[place_id]["name"]
           ~~~~~~^^^^^^^^^^
KeyError: 99

Read it in this order. Last line: KeyError: 99 — the kind of failure and the offending value. Then walk up to the deepest frame in a file you wrote: /app/main.py, line 42. Everything above that is library code doing its job; it is context, not cause. So: something asked for place 99, and 99 is not in the dictionary.

That is the whole method, and it works on every traceback you will ever see. The frames from site-packages are almost never where your bug is — they are the path the request took to reach your mistake.

  • The last line is a sentence, so read it as one. KeyError: 99, TypeError: unsupported operand type(s) for +: 'int' and 'str', ConnectionRefusedError — each names both the category and the specific value. Most of the answer is already there.
  • Search the error type, not your whole traceback. Searching KeyError: 99 finds nothing useful. Searching "python KeyError dict missing key" finds the concept.
  • Print beats guessing. One print(place_id, type(place_id), places.keys()) above the failing line answers more than twenty minutes of staring. Module 10 replaces prints with real logging; until then, print freely.
  • ConnectionRefusedError almost always means the other thing is not running. Database, server, container. Check docker compose ps first.
Practice deliberately. Take working code and break it four ways: misspell a variable, remove an import, ask for a key that does not exist, add a string to a number. Read each traceback before fixing it. Twenty minutes spent here saves days later, because you stop treating errors as noise and start treating them as the most direct information the computer ever gives you.

Build this

Target: a folder called city-guide that contains a git repository with at least two commits, an active virtual environment with FastAPI installed, a Postgres container you can open a SQL prompt against, and a .env file that git refuses to track. Nothing runs yet — this is the ground the next twelve modules are built on.
  1. Create the folder, run git init, and write .gitignore before anything else.
  2. Create and activate .venv, install fastapi and uvicorn, write requirements.txt.
  3. Write docker-compose.yml and bring Postgres up. Open psql and run SELECT version();.
  4. Write .env and .env.example. Confirm git status does not list .env.
  5. Commit. Then break something on purpose: run docker compose down, try to connect, and read the error you get.

Verify it yourself

Run each command. If the output does not match, the module is not finished — and that is useful information, not a failure.

$ pwd
/…/city-guide                       # you are in the project folder

$ git status --short
                                     # empty output = everything committed

$ git check-ignore -v .env
.gitignore:1:.env	.env             # proof .env is ignored. No output = BROKEN

$ source .venv/bin/activate && python -c "import fastapi; print(fastapi.__version__)"
0.128.8                              # any version number is fine - the point is that it prints
                                     # one. ModuleNotFoundError means the venv is not active.

$ docker compose ps
NAME               IMAGE         ...   STATUS         PORTS
city-guide-db-1    postgres:16   ...   Up 2 minutes   0.0.0.0:5432->5432/tcp
                                       # columns trimmed here to fit; STATUS must say Up

$ docker compose exec db psql -U cityguide -d cityguide -c "SELECT 1 AS ok;"
 ok
----
  1
(1 row)                                # the database answered

$ docker compose down && docker compose exec db psql -U cityguide -c "SELECT 1;"
service "db" is not running            # the deliberate break. Now bring it back up.
A socket error on your first psql is normal. If you see connection to server on socket "/var/run/postgresql/.s.PGSQL.5432" failed: No such file or directory, the container is up but Postgres inside it has not finished starting. Wait a few seconds and run it again, or wait for it properly with docker compose exec db pg_isready -U cityguide. This gap between "container started" and "database ready" is the same one that causes a whole class of bug in Module 11.
If git check-ignore prints nothing, stop and fix it now. That silence means .env is trackable, and the first time you put a real key in it you will commit it. This is the one check in this module with consequences beyond your laptop.

Prove it

Answer each in writing, in your own words, in notes/module-00.md. Then open the answer and compare. Where yours differs, work out which of you is right — that is where the learning is.

1. What does activating a virtual environment actually change?

Show answer

It changes your shell's PATH so that python and pip resolve to the copies inside .venv/bin instead of the system ones. Nothing is installed or moved; a single environment variable is rewritten for that terminal session. That is also why it applies per terminal window, why deactivate restores the old value instantly, and why which python is the fastest way to check whether it worked.

2. Your app connects to localhost:5432, but Postgres is inside a container. Why does that work?

Show answer

Because of the published port. ports: - "5432:5432" tells Docker to listen on port 5432 of your machine and forward anything arriving there into port 5432 of the container. Your app never talks to the container directly — it talks to your own machine, and Docker relays it. Remove that line and the database still runs perfectly, but nothing outside the container can reach it. (In Module 11, when your app is also a container, it will reach the database by service name instead, and no published port is needed at all.)

3. You committed a file containing a real password, noticed, deleted it, and committed again. Is the password safe?

Show answer

No. Git stores history, not just the current state — the old commit still contains the file, and anyone with the repository can read it with git log -p. Deleting it in a later commit hides it from the current checkout and nothing else. The only correct response is to treat that password as compromised and rotate it: change it at the source. Rewriting history is possible but secondary, and useless if the repository was ever pushed or shared. This is why .gitignore is written before the first commit.

4. What is the difference between docker compose down and docker compose down -v?

Show answer

down stops and removes the containers but leaves named volumes alone, so pgdata survives and your tables and rows are still there when you come back up. down -v also deletes those volumes, which permanently destroys the database contents. The first is "turn it off", the second is "start again from nothing". You will want the second more often than you expect while learning — a broken practice schema is not worth debugging.

5. Given this traceback, where is the bug and what is it?

  File "/app/.venv/lib/python3.12/site-packages/starlette/routing.py", line 72, in app
    await response(scope, receive, send)
  File "/app/main.py", line 18, in create_place
    total = place["rating"] + 1
TypeError: can only concatenate str (not "int") to str
Show answer

Line 18 of main.py, the file you wrote — the starlette frame above it is library code that called you, not the cause. The message says the left operand is a str, so place["rating"] arrived as text like "4" rather than the number 4. The immediate fix is int(place["rating"]), but the real fix is to stop untyped values entering the function at all — which is exactly the job of the request models in Module 06.