software-engineering-playbook

🧠 Software Engineering Playbook

A complete methodology for building software projects: how to think and build from scratch to deployment, from CLI tools to full-stack apps.

License: MIT PRs Welcome


πŸ“Œ About This Playbook

Most developers don’t get stuck because they lack tools. They get stuck because they lack a repeatable process. This playbook is that process, written down: a fixed sequence of steps that takes you from a blank folder to a deployed, tested, documented project.

Who it’s for

What’s inside

Stack at a glance

Path Stack Best for
Classic Python, Flask / Streamlit, SQLAlchemy, MySQL / PostgreSQL Fast portfolio projects, dashboards
Modern full-stack FastAPI, React, PostgreSQL, Alembic, Docker Production-style apps with a separate frontend
CLI Python stdlib, argparse, JSON Small tools, learning fundamentals

How to use it

  1. Read Part A once, since it changes how you approach projects.
  2. Before every project, walk through Part B in order.
  3. Use Part C as a reference while you work (Git, files, CI).
  4. Use Section 10 when you’re ready to ship.
  5. Copy the checklists in Part E into each new project.

πŸ“– Table of Contents


PART A β€” The Philosophy

1. Why You Get Stuck: The Real Problem

You say: β€œI don’t know the process well enough yet.”

That’s the right starting point. Every junior developer feels the same. The difference is that most think the problem is:

Wrong. The real problem is:

You haven’t internalized the PROCESS. You know Python, Flask, FastAPI, SQL, React β€” but you don’t know the sequence of steps that takes you from blank to finished.

It’s like knowing how to use a hammer, nails, and a saw β€” but not knowing how to build a table. You know the tools, not the method.

What This Guide Will Change

Before:

After:

The difference: You think first, then you write.


2. The 3 Phases of Thinking

Every project goes through 3 phases of thinking:

🧠 Phase 1: Abstraction

β€œWhat do I want to do, in plain words?”

You think at a high level. You don’t touch code.

Example: β€œI want a CLI that tracks expenses and shows me totals.”

πŸ”§ Phase 2: Decomposition

β€œWhat small pieces does it break into?”

You split the big thing into small pieces. Each piece becomes a module or a function.

Example:

Expense Tracker CLI:
β”œβ”€β”€ Add expense
β”œβ”€β”€ View expenses
β”œβ”€β”€ Summary by category
└── Persistence (JSON)

πŸ’» Phase 3: Implementation

β€œHow do I turn this into code?”

You write actual code. The thinking is already done β€” now you execute.

Example: expense_tracker.py, storage.py, models.py


πŸ”‘ Rule: 80% thinking, 20% coding.

Juniors do the opposite: 5% thinking, 95% coding β†’ they rewrite everything 3 times.


PART B β€” The Process (9 Steps)

3. Understanding & Decomposition

3.1 The 6 Questions Before Writing a Single Line

Write the answers in a NOTES.md before opening the editor.

# Question Example (Expense Tracker)
1 What problem am I solving? (1 sentence) β€œI want to track expenses and see totals.”
2 Who uses it and how? Me, via terminal
3 Inputs? amount: float, category: str, description: str
4 Outputs? Expense list, totals, messages
5 Constraints? amount > 0, category from a list, persistence
6 Edge cases? amount=”abc”, empty list, huge number

πŸ’‘ For web apps, also answer: What are the API contracts (endpoints, request/response shapes)? Who is authenticated, and what can each user see? Which secrets does the app need?

3.2 Decomposition β€” The Split

Rule: If you can’t describe a sub-problem in 1-2 sentences, it’s too big.

Example:

"Track and analyze expenses"
β”œβ”€β”€ 1. Add an expense
β”‚   β”œβ”€β”€ 1a. Validate amount
β”‚   β”œβ”€β”€ 1b. Validate category
β”‚   └── 1c. Save
β”œβ”€β”€ 2. View expenses
β”œβ”€β”€ 3. Summary by category
└── 4. Persistence (JSON)

Each leaf β†’ one function or one class.

3.3 When You Know Decomposition Is Done

If not β†’ split more.

3.4 Decomposing a Full-Stack App

For a full-stack project, split first by layer, then by feature:

Full-Stack Expense Tracker
β”œβ”€β”€ Backend (FastAPI + SQLAlchemy + Alembic)
β”‚   β”œβ”€β”€ Auth (JWT / OAuth)
β”‚   β”œβ”€β”€ Expenses CRUD & Caching
β”‚   └── Database models & Migrations
└── Frontend (React + Tailwind + Vitest)
    β”œβ”€β”€ Authentication UI
    β”œβ”€β”€ Dashboard & Charts
    └── API Integration (Fetch/Axios)

4. Algorithmic Thinking

4.1 Pseudocode

Write the logic in plain English, using fixed keywords:

FUNCTION add_expense(amount, category):
    IF amount <= 0 THEN
        RETURN error
    
    IF category NOT IN valid_list THEN
        RETURN error
    
    CREATE new expense
    APPEND to list
    RETURN confirmation

Why: You think about the logic without worrying about Python syntax. When you write it in Python, the translation is nearly 1-to-1.

4.2 Dry Run β€” β€œExecuting” on Paper

Take the pseudocode and β€œrun” it by hand:

Input: amount = -5, category = "Food"
Step 1: -5 <= 0 β†’ YES
Step 2: Returns error "Amount must be positive"
βœ… Correct.

Rule: Dry run with 3 scenarios:


5. Design

5.1 UML Class Diagram

Shows:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚      Expense        β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ - amount: float     β”‚
β”‚ - category: str     β”‚
β”‚ - date: date        β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ + to_dict() -> dict β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚     ExpenseTracker          β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ - expenses: list[Expense]   β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ + add_expense(...)          β”‚
β”‚ + get_summary()             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Questions:

  1. What β€œthings” (nouns) exist? β†’ Classes
  2. What data β€œbelongs” to each thing?
  3. What actions (verbs) does it do?
  4. Do I need a class or a function? (If no state, a function is enough.)

5.2 Modular Architecture

Each file = one responsibility (SRP).

Classic layout (Flask / Streamlit / CLI):

project/
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ __init__.py
β”‚   β”œβ”€β”€ main.py            # Entry point
β”‚   β”œβ”€β”€ models/            # Domain models
β”‚   β”œβ”€β”€ services/          # Business logic
β”‚   β”œβ”€β”€ routes/            # HTTP endpoints
β”‚   β”œβ”€β”€ storage/           # Persistence
β”‚   └── ui/                # Presentation
β”œβ”€β”€ tests/
β”œβ”€β”€ requirements.txt
β”œβ”€β”€ .env.example
β”œβ”€β”€ .gitignore
└── README.md

Full-stack layout (FastAPI + React):

project/
β”œβ”€β”€ backend/
β”‚   β”œβ”€β”€ src/
β”‚   β”‚   β”œβ”€β”€ models/       # SQLAlchemy models
β”‚   β”‚   β”œβ”€β”€ schemas/      # Pydantic schemas
β”‚   β”‚   β”œβ”€β”€ services/     # Business logic
β”‚   β”‚   β”œβ”€β”€ routers/      # FastAPI endpoints
β”‚   β”‚   └── core/         # Config, database, security
β”‚   β”œβ”€β”€ alembic/          # Database migrations
β”‚   β”œβ”€β”€ tests/            # Pytest (unit & integration)
β”‚   β”œβ”€β”€ Dockerfile
β”‚   └── requirements.txt
β”œβ”€β”€ frontend/
β”‚   β”œβ”€β”€ src/              # React components & hooks
β”‚   β”œβ”€β”€ tests/            # Vitest / E2E tests
β”‚   └── package.json
β”œβ”€β”€ docker-compose.yml
β”œβ”€β”€ .env.example
β”œβ”€β”€ .gitignore
└── README.md

Key Question: β€œIf I change storage (e.g. JSON β†’ DB), how many files do I touch?”

5.3 Data & API Design (Web Apps)

Before writing endpoints, write the contract:

Method Path Auth Request Response
POST /auth/register No {email, password} 201 {id, email}
POST /auth/login No {email, password} 200 {access_token}
GET /expenses JWT β€” 200 [Expense]
POST /expenses JWT {amount, category} 201 Expense

Also sketch an ERD (tables, primary keys, foreign keys) before creating any models.


6. Implementation

6.1 Rules for Functions

  1. One function, one job (SRP)
  2. Name = verb (add_expense, not expense_stuff)
  3. Type hints always
  4. Docstring on every non-obvious function
def add_expense(
    tracker: "ExpenseTracker",
    amount: float,
    category: str,
    description: str = "",
) -> "Expense":
    """Add a new expense after validating it.
    
    Args:
        tracker: The tracker to add to.
        amount: Amount > 0.
        category: Category from the allowed list.
        description: Optional.
    
    Returns:
        The new Expense.
    
    Raises:
        ValueError: If amount <= 0 or category is unknown.
    """
    if amount <= 0:
        raise ValueError("Amount must be positive")
    ...

6.2 Rules for Classes

  1. Represents a β€œthing” (not just a bag of functions)
  2. __init__ = initialization only (no heavy logic)
  3. Encapsulation β€” underscore for private attributes
  4. A class shouldn’t know too much about other classes

6.3 When to Use OOP vs Functions

Need a class if… Need a function if…
It has state (self.x) No state
You want inheritance No relation to others
You want many objects of the same type Pure logic

Example:

6.4 Configuration & Secrets

# core/config.py
import os
from dotenv import load_dotenv

load_dotenv()

class Settings:
    DATABASE_URL: str = os.getenv("DATABASE_URL", "sqlite:///./dev.db")
    SECRET_KEY: str = os.getenv("SECRET_KEY", "")
    DEBUG: bool = os.getenv("DEBUG", "false").lower() == "true"

settings = Settings()

6.5 Error Handling

6.6 Database Migrations

alembic revision --autogenerate -m "add expenses table"
alembic upgrade head      # apply
alembic downgrade -1      # roll back one step

6.7 Security Basics


7. Testing & Debugging

7.1 Testing β€” 3 Tests per Function

For every function, write at least 3 tests:

def test_add_expense_happy_path():
    """Normal scenario."""
    tracker = ExpenseTracker()
    tracker.add_expense(50.0, "Food")
    assert len(tracker.expenses) == 1

def test_add_expense_rejects_negative():
    """Edge case: negative."""
    with pytest.raises(ValueError):
        ExpenseTracker().add_expense(-10, "Food")

def test_add_expense_rejects_unknown_category():
    """Invalid input."""
    with pytest.raises(ValueError):
        ExpenseTracker().add_expense(10, "Unknown")

Why 3:

7.2 The Testing Pyramid

        /\
       /E2E\        few   β€” full user flows (slow)
      /------\
     /Integr. \     some  β€” API + database together
    /----------\
   /   Unit     \   many  β€” single functions (fast)
  /--------------\
Layer Tool (backend) Tool (frontend) What it checks
Unit pytest Vitest One function / component
Integration pytest + test DB (SQLite in-memory) React Testing Library Layers working together
E2E pytest + httpx / Playwright Playwright / Cypress Real user flows

Tips:

7.3 Debugging β€” 5 Steps

  1. Read the traceback β€” bottom to top
  2. Isolate β€” comment out pieces
  3. Print/Log intermediate values
  4. Form a hypothesis β€” β€œI think it’s X” β†’ test it
  5. 20-25 minute rule β€” if stuck, ask for help

7.4 Logging vs Print

print() disappears. logging keeps history:

from loguru import logger

logger.info(f"Added expense: {amount}")
logger.warning(f"Invalid amount: {amount}")
logger.error(f"Failed to save: {e}")

Levels:


8. Refactoring

Refactoring = Change the STRUCTURE, not the BEHAVIOR.

⚠️ Do it AFTER the tests are green β€” tests are your safety net.

Questions:

  1. Code that repeats 2+ times? β†’ Function
  2. Function > 20 lines? β†’ Split it
  3. Magic numbers? β†’ Constants
  4. Bad names? β†’ Rename
  5. PEP 8? β†’ black, ruff

8.1 Automate Quality with Pre-commit

Catch formatting and lint issues before they reach Git:

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.6.9
    hooks:
      - id: ruff
        args: [--fix]
      - id: ruff-format
pip install pre-commit
pre-commit install

9. Documentation

9.1 Docstrings vs Comments

Docstrings β†’ the β€œwhat”:

def add_expense(...):
    """Adds a new expense."""

Comments β†’ the β€œwhy”:

# BAD: states the obvious
i += 1  # increments i

# GOOD: explains why
i = 1  # skip header row

9.2 README Structure

# Project Name
Short description.

## Features
- ...

## Tech Stack
- ...

## Installation
```bash
git clone ...
pip install -r requirements.txt
```

## Usage
```bash
python main.py
```

## Screenshots
![...](...)

## Testing
```bash
pytest tests/ -v
```

## License
MIT

9.3 API Documentation

FastAPI generates interactive docs automatically. Link them in your README:

For Flask, document endpoints in the README as a table (method, path, auth, description).


10. Deployment

Deployment is not the last-minute step. It’s a skill with its own checklist. Follow the sections below in order.

10.1 Before Deploy β€” Checklist

10.2 Deployment Options

Project Type Where Time Free Tier
Streamlit Streamlit Community Cloud 5 min βœ…
Flask Render / Railway 10 min βœ… (sleeps when idle)
FastAPI Render / Railway 10 min βœ… (sleeps when idle)
CLI GitHub only (or PyPI) β€” βœ…
Frontend (React/Vite) Vercel / Netlify / GitHub Pages 5 min βœ…
Database (PostgreSQL) Neon / Supabase 5 min βœ…
Full Stack Vercel + Render + Neon 30 min βœ…

⚠️ Free tiers change often. Check each provider’s current limits and pricing before you rely on them.

10.3 The Deployment Order for Full-Stack Apps

Always deploy from the bottom up, because each layer needs the one below it:

1. Database   (Neon / Supabase)   β†’ get DATABASE_URL
        β”‚
        β–Ό
2. Backend    (Render / Railway)  β†’ get API URL
        β”‚
        β–Ό
3. Frontend   (Vercel / Netlify)  β†’ set VITE_API_URL to the API URL
        β”‚
        β–Ό
4. Back to backend β†’ set CORS_ORIGINS to the frontend URL

10.4 Step 1 β€” Deploy the Database (Neon or Supabase)

  1. Create an account at neon.tech or supabase.com.
  2. Create a new project and pick the region closest to your backend.
  3. Copy the connection string.
  4. Make sure it matches your driver and requires SSL:
postgresql+psycopg2://USER:PASSWORD@HOST/DBNAME?sslmode=require
  1. Save it as DATABASE_URL. You’ll paste it into your backend host in the next step.
  2. Run migrations against it once (from your machine, with the production URL exported temporarily):
export DATABASE_URL="postgresql+psycopg2://..."
alembic upgrade head

πŸ’‘ Use connection pooling (Neon’s pooled connection string, or Supabase’s pooler) for serverless/free-tier setups to avoid exhausting connections.

10.5 Step 2 β€” Deploy the Backend (Render)

Prepare the repo:

@app.get("/health")
def health():
    return {"status": "ok"}

On Render:

  1. Go to render.com β†’ New β†’ Web Service β†’ connect your GitHub repo.
  2. Configure:
Setting FastAPI Flask
Root Directory backend (if monorepo) repo root or subfolder
Runtime Python 3 Python 3
Build Command pip install -r requirements.txt pip install -r requirements.txt
Start Command uvicorn src.main:app --host 0.0.0.0 --port $PORT gunicorn "src:create_app()" --bind 0.0.0.0:$PORT
Health Check Path /health /health
  1. Under Environment, add your variables:
DATABASE_URL=postgresql+psycopg2://...?sslmode=require
SECRET_KEY=<long random string>
DEBUG=false
CORS_ORIGINS=http://localhost:5173
PYTHON_VERSION=3.12.0

Generate a strong secret locally:

python -c "import secrets; print(secrets.token_urlsafe(48))"
  1. Click Create Web Service. Wait for the build, then open https://your-service.onrender.com/docs (FastAPI) or /health.
  2. Copy the service URL. The frontend needs it.

⚠️ Free tier note: free web services sleep after a period of inactivity, so the first request can take 30-60 seconds. Mention this in your README so recruiters aren’t surprised.

Running migrations on deploy: if your plan supports a pre-deploy command, set it to alembic upgrade head. Otherwise, chain it into the start command:

alembic upgrade head && uvicorn src.main:app --host 0.0.0.0 --port $PORT

Railway alternative: New Project β†’ Deploy from GitHub repo β†’ add the same environment variables β†’ set the start command. The steps are nearly identical.

10.6 Step 3 β€” Deploy the Frontend (Vercel)

  1. Go to vercel.com β†’ Add New β†’ Project β†’ import your GitHub repo.
  2. Configure:
Setting Value
Root Directory frontend
Framework Preset Vite (or Create React App / Next.js)
Build Command npm run build
Output Directory dist (Vite) or build (CRA)
  1. Add the environment variable pointing at your backend:
VITE_API_URL=https://your-service.onrender.com
  1. Click Deploy. Vercel gives you a URL like https://your-app.vercel.app.

In the frontend, read the URL from the environment, never hardcode it:

const API_URL = import.meta.env.VITE_API_URL;
const res = await fetch(`${API_URL}/expenses`);

πŸ’‘ Vite variables must start with VITE_ to be exposed to the browser. Never put secrets in them; everything in the frontend is public.

10.7 Step 4 β€” Connect Them (CORS)

The browser blocks requests from your frontend to your backend unless the backend allows that origin. Go back to Render and set:

CORS_ORIGINS=https://your-app.vercel.app

And in FastAPI:

import os
from fastapi.middleware.cors import CORSMiddleware

origins = os.getenv("CORS_ORIGINS", "").split(",")

app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

Redeploy the backend. Never use allow_origins=["*"] together with credentials in production.

10.8 Deploying a Streamlit App

  1. Push the repo to GitHub. The entry file (e.g. app.py) and requirements.txt must be in the repo.
  2. Go to share.streamlit.io β†’ New app.
  3. Select the repository, branch (main), and main file path (app.py).
  4. Open Advanced settings β†’ Secrets and paste your secrets in TOML format:
DATABASE_URL = "postgresql://USER:PASSWORD@HOST/DBNAME?sslmode=require"
API_KEY = "your_api_key_here"
  1. Click Deploy. Your app goes live at https://your-app.streamlit.app.

Read secrets lazily (inside functions, not at import time) so tests and CI don’t fail with StreamlitSecretNotFoundError:

import streamlit as st

def get_database_url() -> str:
    return st.secrets["DATABASE_URL"]

Docker guarantees β€œit works on my machine” also means β€œit works everywhere.”

Backend Dockerfile:

FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["sh", "-c", "uvicorn src.main:app --host 0.0.0.0 --port ${PORT:-8000}"]

docker-compose.yml for local development:

services:
  db:
    image: postgres:16
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: dev
      POSTGRES_DB: mydb
    ports: ["5432:5432"]
    volumes:
      - pgdata:/var/lib/postgresql/data

  backend:
    build: ./backend
    ports: ["8000:8000"]
    env_file: .env
    depends_on: [db]

  frontend:
    build: ./frontend
    ports: ["5173:5173"]
    depends_on: [backend]

volumes:
  pgdata:

Verify locally before deploying:

docker compose up --build

Render and Railway can also deploy straight from your Dockerfile. Choose Docker as the runtime instead of Python.

10.10 After Deploy β€” Verification Checklist

10.11 Updating & Rolling Back

Update: merge to main β†’ Render and Vercel redeploy automatically on push.

Rolling back:

10.12 Deployment Troubleshooting

Symptom Likely Cause Fix
Build fails: ModuleNotFoundError Dependency missing from requirements.txt Add it and push
App crashes: KeyError: DATABASE_URL Env var not set on the host Add it in the dashboard, redeploy
Address already in use / port error Not binding to $PORT Use --port $PORT --host 0.0.0.0
SSL connection required Missing sslmode=require Append it to DATABASE_URL
Frontend shows CORS error Backend doesn’t allow the frontend origin Set CORS_ORIGINS, redeploy backend
Frontend calls localhost in production Hardcoded API URL Use VITE_API_URL, redeploy frontend
relation "x" does not exist Migrations not applied Run alembic upgrade head on production
First request takes ~1 minute Free-tier cold start Expected; note it in the README
Works locally, fails on the host Python version mismatch Pin PYTHON_VERSION
Streamlit StreamlitSecretNotFoundError Secrets read at import time Read lazily; add secrets in app settings

11. Practice

The 3 Levels

Level 1 β€” Daily (15-20 min)

Level 2 β€” Weekly (small projects)

Level 3 β€” Core projects

Habits

  1. Re-read old code after 1 month
  2. Explain your code to someone (rubber duck)
  3. Read others’ code on GitHub
  4. Keep a postmortem β€” β€œwhat would I change?”
  5. Timed practice β€” 30 min/kata

🌱 The truth: Comfort comes around the 15th-20th project, not the 3rd. Don’t get discouraged.


PART C β€” The Technical Workflow

12. Git Workflow: Branches & Commits

12.1 The Branch Rule

Every feature = new branch. NEVER commit directly to main.

12.2 Branch Naming

Prefix When Example
chore/ Maintenance chore/initial-setup
feat/ New feature feat/storage-layer
fix/ Bug fix fix/config-ssl
test/ Tests test/unit-tests
docs/ Documentation docs/readme
refactor/ Refactor refactor/service-layer

12.3 The Flow (for every feature)

# 1. Sync
git checkout main
git pull

# 2. New branch
git checkout -b feat/feature-name

# 3. Work + commits (atomic!)
git add <file>
git commit -m "feat(scope): description"

# 4. Merge back
git checkout main
git merge --no-ff feat/feature-name -m "merge: description"
git branch -d feat/feature-name

# 5. Push
git push

πŸ’‘ Team / PR workflow: instead of step 4, push the branch and open a Pull Request on GitHub. CI runs, you review, then merge via the GitHub UI.

12.4 Why --no-ff

Without: Flat history β†’ you lose the structure. With: Creates a merge commit β†’ branches are visible in git log --graph.

12.5 Commit Messages β€” Conventional Commits

Format:

<type>(<scope>): <short description>

Types:

Examples:

chore: add .gitignore for Python
feat(config): add env loader
feat(storage): add JSON file storage
test(storage): add unit tests
docs: rewrite README with screenshots
fix(config): correct indentation
merge: add storage layer

β›” BAD:

"fixed stuff"
"wip"
"asdf"
"update"

βœ… GOOD:

"feat(auth): add bcrypt password hashing"
"fix(api): handle 404 from GitHub API"
"test(tasks): add edge cases for empty input"

12.6 Atomic Commits β€” The Golden Rule

One commit = one logical change.

If you revert, you lose one thing β€” not five.

Example:

12.7 The Typical Branch Sequence for One Project

# Branch What You Do Commits
1 chore/initial-setup .gitignore, .env.example, README.md, folders 4-5
2 chore/requirements-and-config requirements.txt, config.py 2-3
3 feat/database-models SQLAlchemy models 2
4 feat/database-connection Engine + session 1-2
5 feat/services-layer Business logic 2-3
6 feat/routes-and-templates HTTP + UI 2-3
7 test/unit-tests Tests 2-3
8 chore/ci GitHub Actions 1
9 docs/final-readme README + screenshots 1-2
10 chore/add-license LICENSE 1

Total: ~20-25 commits, 10 branches.

12.8 When to Commit

Every time you:

Not:

12.9 Handy Git Commands

git status                      # what changed?
git log --oneline --graph       # visual history
git diff --staged               # review before committing
git restore --staged <file>     # unstage a file
git stash / git stash pop       # park work temporarily
git revert <commit>             # safely undo a commit (new commit)

13. The Essential Files of Every Project

13.1 .gitignore β€” First file!

Create it BEFORE anything else.

# Virtual Environment
.venv/
venv/
env/

# Secrets
.env
*.env

# Python cache
__pycache__/
*.pyc
*.pyo
.pytest_cache/
.ruff_cache/
.coverage
htmlcov/
*.egg-info/
build/
dist/

# Node (full-stack projects)
node_modules/

# OS junk
.DS_Store
Thumbs.db

# IDE
.vscode/
.idea/

# App data
data/
*.log
*.db
tasks.json

Why first: Prevents accidental commits.

⚠️ *.env also ignores .env.example-style names only if they end in .env. .env.example is not matched, so it stays tracked. Verify with git status.

13.2 .env.example β€” Template

What others need, WITHOUT the real secrets.

# Flask
SECRET_KEY=your_secret_key_here
FLASK_DEBUG=True

# Database
DB_HOST=localhost
DB_PORT=3306
DB_USER=root
DB_PASSWORD=your_password_here
DB_NAME=myapp
DATABASE_URL=mysql+pymysql://root:password@localhost:3306/myapp

# Web (full-stack)
CORS_ORIGINS=http://localhost:5173
VITE_API_URL=http://localhost:8000

Rule: Placeholders always. NEVER real credentials.

13.3 .env β€” Real Secrets

NOT pushed to Git (it’s in .gitignore).

SECRET_KEY=<generated, long and random>
DB_PASSWORD=<your real password>
DATABASE_URL=mysql+pymysql://root:<your real password>@localhost:3306/myapp

Creation: Copy-Item .env.example .env (Windows PowerShell) or cp .env.example .env (macOS/Linux) β†’ fill in.

13.4 requirements.txt β€” Runtime

Flask>=3.0.0
SQLAlchemy>=2.0.0
python-dotenv>=1.0.0

13.5 requirements-dev.txt β€” Development

-r requirements.txt
pytest>=7.4.0
pytest-mock>=3.11.0
pytest-cov>=5.0.0
black>=24.0.0
ruff>=0.3.0

Why separate: Users who pip install don’t need pytest.

13.6 .venv/ β€” Virtual Environment

Creation:

python -m venv .venv
.venv\Scripts\Activate.ps1   # Windows
source .venv/bin/activate    # macOS/Linux

Why: Isolation β€” each project has its own packages.

⚠️ NEVER in Git (it’s in .gitignore).

13.7 README.md β€” The Showcase

Structure:

  1. Title + tagline
  2. Badges (Python, License, Tests, CI)
  3. Live Demo link
  4. Screenshots
  5. About (why you built it)
  6. Features (what it does)
  7. Tech Stack (what you use)
  8. Installation (how to run it)
  9. Usage (examples)
  10. Testing (how to run tests)
  11. Project Structure
  12. Deployment (where it runs, env vars needed)
  13. License

If you count 30 seconds: The recruiter reads the README before the code.

MIT License (most common for portfolios):

MIT License

Copyright (c) 2026 Your Name

Permission is hereby granted, free of charge, ...

13.9 pyproject.toml β€” Modern Packaging

[build-system]
requires = ["setuptools>=68.0"]
build-backend = "setuptools.build_meta"

[project]
name = "task-cli"
version = "0.1.0"
description = "A simple CLI task manager"
requires-python = ">=3.10"
license = { text = "MIT" }

[project.scripts]
task = "task_cli.main:main"

[tool.pytest.ini_options]
testpaths = ["tests"]

[tool.ruff]
line-length = 88

Why: pip install -e . β†’ command task available everywhere.

13.10 Optional: Dockerfile + docker-compose.yml

See Section 10.9 for complete, ready-to-use files.

13.11 Optional: Makefile β€” Shortcuts

install:
	pip install -r requirements-dev.txt

test:
	pytest tests/ -v --cov=src

lint:
	ruff check . && black --check .

format:
	black . && ruff check --fix .

run:
	uvicorn src.main:app --reload

Now make test replaces a long command you’d otherwise forget.


14. CI/CD Pipeline

14.1 What CI Is

Continuous Integration = automatic tests on every push.

If they pass β†’ βœ… If they fail β†’ ❌ notification

Continuous Deployment (CD) = passing code is deployed automatically. Render and Vercel do this for you when connected to GitHub: push to main β†’ auto-deploy.

14.2 GitHub Actions β€” tests.yml

Location: .github/workflows/tests.yml

name: Tests

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    name: Python $
    runs-on: ubuntu-latest
    
    strategy:
      fail-fast: false
      matrix:
        python-version: ["3.10", "3.11", "3.12"]
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Set up Python $
        uses: actions/setup-python@v5
        with:
          python-version: $
          cache: pip
      
      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -r requirements.txt
          pip install pytest
      
      - name: Lint
        run: |
          pip install ruff
          ruff check .
      
      - name: Run tests
        run: pytest tests/ -v

14.3 Testing Against a Real Database in CI (Optional)

    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_PASSWORD: test
          POSTGRES_DB: testdb
        ports: ["5432:5432"]
        options: >-
          --health-cmd "pg_isready -U postgres"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    env:
      DATABASE_URL: postgresql+psycopg2://postgres:test@localhost:5432/testdb

Run alembic upgrade head before pytest to test migrations too.

14.4 Frontend CI (Vitest)

  frontend:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: frontend
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm
          cache-dependency-path: frontend/package-lock.json
      - run: npm ci
      - run: npm test -- --run
      - run: npm run build

14.5 Why Matrix 3 Versions

Shows it works on multiple Python versions β†’ production-ready.

14.6 Badge in README

[![Tests](https://github.com/USER/REPO/actions/workflows/tests.yml/badge.svg)](https://github.com/USER/REPO/actions)

Will show:

14.7 When to Add CI

After:

Before:

14.8 Troubleshooting β€” Common Issues

Error Cause Fix
StreamlitSecretNotFoundError st.secrets at import Lazy init
ModuleNotFoundError: src Missing pyproject.toml Add config
pytest: not found Missing from requirements pip install pytest
KeyError: DATABASE_URL Tests want prod config TestConfig

Lesson: CI reveals design flaws before deploy.


15. Screenshots & Presentation

15.1 When to Take Screenshots

AFTER the project works live and has no bugs.

Before you:

15.2 How Many and Where

Project Type How Many What
Web app 3-4 Home + 2-3 features
CLI tool 2-3 --help + commands + output
API 1-2 Swagger UI + 1 request

15.3 How (Windows)

Method How
Win + Shift + S Snipping tool
Win + PrtScn Full screen β†’ auto-save
Alt + PrtScn Active window only

15.4 Where to Store Them

project/
β”œβ”€β”€ docs/
β”‚   └── screenshots/
β”‚       β”œβ”€β”€ home.png
β”‚       β”œβ”€β”€ feature1.png
β”‚       └── feature2.png

In README:

## πŸ“Έ Screenshots

| Home | Feature 1 |
|---|---|
| ![Home](docs/screenshots/home.png) | ![Feature](docs/screenshots/feature1.png) |

⚠️ On GitHub: Relative paths (docs/...), NOT absolute.

15.5 Best Practices

Put the live demo URL and one screenshot at the very top of the README, right under the title. Visitors decide in seconds whether to keep reading.


PART D β€” Case Studies

πŸ“Š Overview

# Project Stack Branches Commits Tests CI
1 Weather Dashboard Streamlit + API 10 ~25 13 βœ…
2 To-Do App Streamlit + PostgreSQL 10 ~30 38 βœ…
3 Flight Search Flask + MySQL 10 ~25 14 βœ…
4 Task CLI Python CLI 7 ~20 33 βœ…
5 Expense Tracker (full-stack) FastAPI + React + PostgreSQL 12+ ~35 pytest + Vitest βœ…

🌀️ Weather Dashboard

Branches:

  1. chore/initial-setup β€” .gitignore, .env.example, README.md, folders
  2. chore/requirements-and-config β€” requirements.txt, config.py
  3. feat/weather-fetcher β€” API client
  4. feat/data-visualizer β€” Plotly charts
  5. feat/history-tracker β€” JSON persistence
  6. feat/streamlit-ui β€” UI
  7. test/unit-tests β€” pytest
  8. docs/final-readme β€” README + screenshots
  9. chore/add-license β€” MIT
  10. chore/ci β€” GitHub Actions

Skills: REST API, OOP, caching, testing, deployment

Deploy: Streamlit Community Cloud (Section 10.8)


βœ… To-Do App

Branches:

  1. chore/initial-setup β€” setup
  2. chore/requirements-and-config β€” deps + config
  3. feat/database-models β€” User, Task
  4. feat/database-connection β€” Engine + session
  5. feat/auth-service β€” bcrypt + register/login
  6. feat/task-service β€” CRUD scoped per user
  7. feat/streamlit-auth-ui β€” Login/Register pages
  8. feat/streamlit-task-ui β€” Task list
  9. test/unit-tests β€” 38 tests
  10. docs/final-readme β€” README + screenshots
  11. chore/add-license β€” MIT
  12. chore/ci β€” GitHub Actions

Skills: Auth, PostgreSQL, per-user isolation, bcrypt, SQLAlchemy 2.0

Deploy: Streamlit Community Cloud + Neon / Supabase (Sections 10.4 and 10.8)


Branches:

  1. chore/initial-setup β€” setup
  2. chore/requirements-and-config β€” deps + config
  3. feat/database-models β€” 6 models + 2 associations
  4. feat/database-connection β€” lazy init engine
  5. feat/services-layer β€” business logic
  6. feat/routes-and-templates β€” Flask blueprints + Jinja2
  7. test/unit-tests β€” pytest + SQLite in-memory
  8. docs/final-readme β€” README + screenshots
  9. chore/add-license β€” MIT
  10. chore/ci β€” GitHub Actions

Skills: Complex SQL, multi-table JOINs, admin auth, Flask blueprints

Deploy: Render + managed database (Section 10.5)


πŸ“ Task CLI

Branches:

  1. chore/initial-setup β€” setup
  2. chore/requirements-and-config β€” pytest only
  3. feat/storage β€” JSON persistence
  4. feat/task-logic β€” business logic
  5. feat/display β€” output formatting
  6. feat/cli β€” argparse entry point
  7. docs/final-readme β€” README + screenshots
  8. chore/ci β€” GitHub Actions

Skills: CLI, argparse, stdlib-only, atomic JSON writes

Deploy: GitHub only (optionally publish to PyPI)


πŸ’Έ Expense Tracker (Full-Stack)

Stack: FastAPI, SQLAlchemy, Alembic, PostgreSQL, React, Tailwind, Docker, pytest, Vitest

Branches:

  1. chore/initial-setup β€” monorepo folders, .gitignore, .env.example
  2. chore/docker-compose β€” Postgres + backend + frontend containers
  3. feat/database-models β€” User, Expense
  4. chore/alembic-migrations β€” first migration
  5. feat/auth β€” JWT register/login, bcrypt
  6. feat/expenses-crud β€” endpoints scoped per user
  7. feat/caching β€” connection pooling + caching strategy
  8. feat/frontend-auth-ui β€” login/register pages
  9. feat/frontend-dashboard β€” charts + expense list
  10. test/backend-tests β€” pytest with DB fixtures
  11. test/frontend-tests β€” Vitest + Testing Library
  12. chore/ci β€” GitHub Actions (backend + frontend jobs)
  13. docs/final-readme β€” architecture, screenshots, live links
  14. chore/deploy β€” Neon + Render + Vercel

Skills: JWT/OAuth, migrations, Docker, full-stack integration, CORS, CI/CD, three-tier deployment

Deploy: Neon (DB) β†’ Render (API) β†’ Vercel (UI), exactly the order in Section 10.3


PART E β€” Checklists & Templates

16. Pre-Project Checklist

Before writing code:

17. Post-Project Checklist

Before deploy:

18. The Mental Model β€” Summary

       IDEA
        β”‚
        β–Ό
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚ 1. UNDERSTANDING β”‚  ← 30 min of thinking
  β”‚    (6 questions) β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
           β”‚
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚ 2. DECOMPOSITION β”‚  ← Split into small pieces
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
           β”‚
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚ 3. PSEUDOCODE    β”‚  ← Logic without syntax
  β”‚    + DRY RUN     β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
           β”‚
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚ 4. DESIGN        β”‚  ← UML, ERD, API contracts
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
           β”‚
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚ 5. IMPLEMENT     β”‚  ← Write code
  β”‚  (branch-based)  β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
           β”‚
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚ 6. TEST          β”‚  ← 3 tests/function
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
           β”‚
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚ 7. REFACTOR      β”‚  ← Clean up
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
           β”‚
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚ 8. DOCUMENT      β”‚  ← README + docstrings
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
           β”‚
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚ 9. CI/CD         β”‚  ← GitHub Actions
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
           β”‚
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚ 10. DEPLOY       β”‚  ← DB β†’ API β†’ UI β†’ CORS
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
           β”‚
           β–Ό
      PORTFOLIO

19. The Golden Rule

Think 80%, write 20%.

One commit = one change.

One branch = one feature.

Tests first, code after.

Never commit secrets.

Deploy from the bottom up: database, backend, frontend.

The README is 50% of the project.

20. The Most Important Thing

Comfort comes from repetition, not from reading.

You’re at #4. You’re not a beginner β€” you’re mid-way.


🀝 Contributing

Found a typo, a better tool, or a missing step? Open an issue or a pull request.

πŸ“„ License

MIT License β€” see LICENSE for details.


Built with β˜• + 🧠 β€” next time you start a project, read this file BEFORE touching the editor. πŸš€