A complete methodology for building software projects: how to think and build from scratch to deployment, from CLI tools to full-stack apps.
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
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.
Before:
After:
The difference: You think first, then you write.
Every project goes through 3 phases of thinking:
β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.β
β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)
β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.
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?
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.
If not β split more.
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)
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.
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:
amount=50, category="Food" β β
addsamount=0 β β
rejectsamount="abc" β β
rejectsShows:
βββββββββββββββββββββββ
β Expense β
βββββββββββββββββββββββ€
β - amount: float β
β - category: str β
β - date: date β
βββββββββββββββββββββββ€
β + to_dict() -> dict β
βββββββββββββββββββββββ
βββββββββββββββββββββββββββββββ
β ExpenseTracker β
βββββββββββββββββββββββββββββββ€
β - expenses: list[Expense] β
βββββββββββββββββββββββββββββββ€
β + add_expense(...) β
β + get_summary() β
βββββββββββββββββββββββββββββββ
Questions:
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?β
storage/) β good designBefore 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.
add_expense, not expense_stuff)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")
...
__init__ = initialization only (no heavy logic)| 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:
ExpenseTracker β class (holds the list of expenses)hash_password(pw) β function (no state)core/config.py) and import it everywhere else.# 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()
ValueError, custom NotFoundError), never bare Exception.except: pass). Log them or re-raise.400 bad input, 401 unauthenticated, 403 forbidden, 404 missing, 422 validation, 500 unexpected).alembic revision --autogenerate -m "add expenses table"
alembic upgrade head # apply
alembic downgrade -1 # roll back one step
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:
/\
/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:
pytest --cov=src (aim for meaningful coverage, not 100%).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:
debug β dev detailsinfo β normal eventswarning β unusualerror β something failedRefactoring = Change the STRUCTURE, not the BEHAVIOR.
β οΈ Do it AFTER the tests are green β tests are your safety net.
Questions:
black, ruffCatch 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
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
# 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
FastAPI generates interactive docs automatically. Link them in your README:
https://your-api.onrender.com/docshttps://your-api.onrender.com/redocFor Flask, document endpoints in the README as a table (method, path, auth, description).
Deployment is not the last-minute step. Itβs a skill with its own checklist. Follow the sections below in order.
rm -rf .venv && python -m venv .venv && pip install -r requirements.txt).env in .gitignorerequirements.txt up to date (pip freeze or manually curated)DEBUG=false)| 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.
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
postgresql+psycopg2://USER:PASSWORD@HOST/DBNAME?sslmode=require
DATABASE_URL. Youβll paste it into your backend host in the next step.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.
Prepare the repo:
requirements.txt includes your server (gunicorn for Flask, uvicorn[standard] for FastAPI).@app.get("/health")
def health():
return {"status": "ok"}
On Render:
| 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 |
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))"
https://your-service.onrender.com/docs (FastAPI) or /health.β οΈ 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.
| 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) |
VITE_API_URL=https://your-service.onrender.com
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.
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.
app.py) and requirements.txt must be in the repo.main), and main file path (app.py).DATABASE_URL = "postgresql://USER:PASSWORD@HOST/DBNAME?sslmode=require"
API_KEY = "your_api_key_here"
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.
/health returns 200/docs) loadsUpdate: merge to main β Render and Vercel redeploy automatically on push.
Rolling back:
alembic downgrade -1 (only if the migration is reversible, so test downgrades locally first).git revert <commit> (creates a new commit; never rewrite main history).| 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 |
Level 1 β Daily (15-20 min)
Level 2 β Weekly (small projects)
Level 3 β Core projects
π± The truth: Comfort comes around the 15th-20th project, not the 3rd. Donβt get discouraged.
Every feature = new branch. NEVER commit directly to
main.
| 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 |
# 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.
--no-ffWithout: Flat history β you lose the structure.
With: Creates a merge commit β branches are visible in git log --graph.
Format:
<type>(<scope>): <short description>
Types:
feat β new featurefix β bug fixdocs β documentationtest β testsrefactor β refactorchore β maintenancestyle β formattingExamples:
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"
One commit = one logical change.
If you revert, you lose one thing β not five.
Example:
feat: add everything β 500 lines, 10 concernsfeat(storage): add JSON storagetest(storage): add tests for JSON storagedocs: document storage format| # | 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.
Every time you:
Not:
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)
.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.
β οΈ
*.envalso ignores.env.example-style names only if they end in.env..env.exampleis not matched, so it stays tracked. Verify withgit status.
.env.example β TemplateWhat 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.
.env β Real SecretsNOT 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.
requirements.txt β RuntimeFlask>=3.0.0
SQLAlchemy>=2.0.0
python-dotenv>=1.0.0
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.
.venv/ β Virtual EnvironmentCreation:
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).
README.md β The ShowcaseStructure:
If you count 30 seconds: The recruiter reads the README before the code.
LICENSE β LegalMIT License (most common for portfolios):
MIT License
Copyright (c) 2026 Your Name
Permission is hereby granted, free of charge, ...
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.
Dockerfile + docker-compose.ymlSee Section 10.9 for complete, ready-to-use files.
Makefile β Shortcutsinstall:
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.
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.
tests.ymlLocation: .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
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.
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
Shows it works on multiple Python versions β production-ready.
[](https://github.com/USER/REPO/actions)
Will show:
passing β all goodfailing β something brokeAfter:
test/unit-tests)Before:
| 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.
AFTER the project works live and has no bugs.
Before you:
| 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 |
| Method | How |
|---|---|
| Win + Shift + S | Snipping tool |
| Win + PrtScn | Full screen β auto-save |
| Alt + PrtScn | Active window only |
project/
βββ docs/
β βββ screenshots/
β βββ home.png
β βββ feature1.png
β βββ feature2.png
In README:
## πΈ Screenshots
| Home | Feature 1 |
|---|---|
|  |  |
β οΈ On GitHub: Relative paths (docs/...), NOT absolute.
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.
| # | 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 | β |
Branches:
chore/initial-setup β .gitignore, .env.example, README.md, folderschore/requirements-and-config β requirements.txt, config.pyfeat/weather-fetcher β API clientfeat/data-visualizer β Plotly chartsfeat/history-tracker β JSON persistencefeat/streamlit-ui β UItest/unit-tests β pytestdocs/final-readme β README + screenshotschore/add-license β MITchore/ci β GitHub ActionsSkills: REST API, OOP, caching, testing, deployment
Deploy: Streamlit Community Cloud (Section 10.8)
Branches:
chore/initial-setup β setupchore/requirements-and-config β deps + configfeat/database-models β User, Taskfeat/database-connection β Engine + sessionfeat/auth-service β bcrypt + register/loginfeat/task-service β CRUD scoped per userfeat/streamlit-auth-ui β Login/Register pagesfeat/streamlit-task-ui β Task listtest/unit-tests β 38 testsdocs/final-readme β README + screenshotschore/add-license β MITchore/ci β GitHub ActionsSkills: Auth, PostgreSQL, per-user isolation, bcrypt, SQLAlchemy 2.0
Deploy: Streamlit Community Cloud + Neon / Supabase (Sections 10.4 and 10.8)
Branches:
chore/initial-setup β setupchore/requirements-and-config β deps + configfeat/database-models β 6 models + 2 associationsfeat/database-connection β lazy init enginefeat/services-layer β business logicfeat/routes-and-templates β Flask blueprints + Jinja2test/unit-tests β pytest + SQLite in-memorydocs/final-readme β README + screenshotschore/add-license β MITchore/ci β GitHub ActionsSkills: Complex SQL, multi-table JOINs, admin auth, Flask blueprints
Deploy: Render + managed database (Section 10.5)
Branches:
chore/initial-setup β setupchore/requirements-and-config β pytest onlyfeat/storage β JSON persistencefeat/task-logic β business logicfeat/display β output formattingfeat/cli β argparse entry pointdocs/final-readme β README + screenshotschore/ci β GitHub ActionsSkills: CLI, argparse, stdlib-only, atomic JSON writes
Deploy: GitHub only (optionally publish to PyPI)
Stack: FastAPI, SQLAlchemy, Alembic, PostgreSQL, React, Tailwind, Docker, pytest, Vitest
Branches:
chore/initial-setup β monorepo folders, .gitignore, .env.examplechore/docker-compose β Postgres + backend + frontend containersfeat/database-models β User, Expensechore/alembic-migrations β first migrationfeat/auth β JWT register/login, bcryptfeat/expenses-crud β endpoints scoped per userfeat/caching β connection pooling + caching strategyfeat/frontend-auth-ui β login/register pagesfeat/frontend-dashboard β charts + expense listtest/backend-tests β pytest with DB fixturestest/frontend-tests β Vitest + Testing Librarychore/ci β GitHub Actions (backend + frontend jobs)docs/final-readme β architecture, screenshots, live linkschore/deploy β Neon + Render + VercelSkills: 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
Before writing code:
.gitignore as the first commitBefore deploy:
pytest -v)docs/screenshots/.env in .gitignore.env.example verified (no real secrets)requirements.txt up to date.github/workflows/git log --graph)docker compose up --build) 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
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.
Comfort comes from repetition, not from reading.
Youβre at #4. Youβre not a beginner β youβre mid-way.
Found a typo, a better tool, or a missing step? Open an issue or a pull request.
MIT License β see LICENSE for details.
Built with β + π§ β next time you start a project, read this file BEFORE touching the editor. π