Skip to main content
← Back to Blog
EngineeringFastAPILangChainRAGPython

The Fundamentals Scratchpad: A Cure for Framework Fuzziness

When your RAG platform grows past 10,000 lines, the basics of FastAPI and LangChain start to feel hazy. Here's the tiny repo that fixes it.

By

A few months into building Cognoir, a multi-tenant RAG platform, I noticed something uncomfortable: I was less fluent in FastAPI than I’d been before I started.

Not in the project-specific code. That part I knew cold — the auth middleware, the tenant resolution, the Celery pipeline for document ingestion, the pgvector upsert patterns. But if you’d asked me to write a fresh FastAPI dependency with a yield-based teardown, or explain what RunnablePassthrough.assign actually does under the hood, I’d have paused.

The framework primitives had gotten buried under project abstractions. I was writing Depends(get_current_user) dozens of times a week without thinking about what Depends actually resolves — because in Cognoir, it’s wrapped inside tenant resolution, which is wrapped inside DB session lifecycle, which is wrapped inside rate limiting. Four layers deep, the primitive disappears.

This is a known problem. Developers who go deep on one codebase lose fluency on the general concepts. It’s a real cost, and it compounds: when something unexpected breaks, you can’t debug efficiently because your mental model of the primitives has drifted.

Here’s what I tried, what didn’t work, and what finally did.

What didn’t work

Re-reading the docs. Obvious first move. Mostly useless, because 95% of what I re-read was stuff I already knew. The 5% I’d forgotten was drowning in the 95% I hadn’t.

Taking a course. Too much overhead for the actual gap. Courses assume you’re starting from zero. I wasn’t at zero, I was at “pretty good but fuzzy in specific places.”

Reading other people’s projects. Interesting but unfocused. Other codebases have their own project-specific abstractions that bury the same primitives in different ways.

Asking an LLM to quiz me. Better than re-reading docs, but passive. The answers came too easily because I was recognizing them, not reconstructing them.

What worked: a tiny scratchpad with a ritual

The thing that finally stuck was a small folder called fundamentals/. Five subfolders — fastapi/, langchain/, rag/, infra/, auth/ — with 15 standalone files. Each file is under 80 lines of real code. Each one demonstrates exactly one primitive in isolation.

Here’s a fragment from fastapi/01_deps.py:

"""
FastAPI Depends — the primitive.

WHY this exists as a scratchpad file:
    In Cognoir, `get_current_user` is wrapped inside tenant resolution, which
    is wrapped inside DB session, which is wrapped inside rate limiting. When
    you need to debug one layer, it's easy to forget that `Depends` is just
    "call this function, cache the result per-request, inject it."
"""
from fastapi import FastAPI, Depends, Header, HTTPException

app = FastAPI()

def get_db():
    db = {"connection": "open"}
    try:
        yield db
    finally:
        db["connection"] = "closed"  # teardown always runs

def verify_token(x_token: str = Header()):
    if x_token != "secret":
        raise HTTPException(status_code=401, detail="bad token")
    return x_token

def get_current_user(token: str = Depends(verify_token), db=Depends(get_db)):
    return {"user_id": 1, "token": token, "db_state": db["connection"]}

@app.get("/items/")
def read_items(user: dict = Depends(get_current_user)):
    return {"user": user}

That’s the whole point of Depends. Four functions, one composition. Nothing about tenants, nothing about rate limits, nothing about async sessions. Just the primitive.

Every file ends with this:

# RE-DERIVATION PROMPT (for when you delete this file and rewrite):
# - Write a route that requires auth + a DB session + query params.
# - The DB session must close even if the route raises.
# - The auth check must happen BEFORE the DB is opened.
# - Bonus: make the DB session dep usable outside FastAPI too.

That prompt is the actual study tool. The file is just the reference you check against after you’ve tried.

The three-part ritual

The scratchpad alone doesn’t work. I’ve made folders like this before and let them rot. What made it stick was a cadence with three tiers.

Weekly, 10 minutes. One command: make rerun-random. Picks a file from the 15. Three minutes reading, five minutes running and tweaking, two minutes skimming the re-derivation prompt. Close the laptop.

Bi-weekly, 30 minutes. Pick one file and actually re-derive it. make rederive FILE=fastapi/01_deps.py archives the original and empties the file. You rewrite it from memory — no docs, no LLM. Then make diff FILE=... and the diff IS your study guide. What you got wrong is what’s fuzzy. What you left out is what you’ve forgotten.

Monthly, 30 minutes. Read framework source. Not tutorials. Source. FastAPI’s dependencies/utils.py demystifies Depends permanently. LangChain’s runnables/base.py makes LCEL stop feeling magical. After reading, update one scratchpad file if your mental model was wrong.

Twelve weeks covers the whole rotation, which matches one quarter. The rhythm is long enough that forgetting actually happens — the whole point — and short enough that I cycle back before the knowledge decays hard.

Why re-derivation beats re-reading

This is the part I want to emphasize, because it took me a while to see it.

When you re-read a file, you recognize the patterns. Recognition feels like understanding, but it isn’t. You close the file thinking “yeah, I got that,” and two weeks later you still can’t write it from scratch.

When you delete the file and rewrite it from memory, you hit the gaps immediately. You sit there typing @asynccontextmanager and realize you can’t remember whether startup code goes before or after the yield. That’s the gap. That’s the thing to fix. The diff afterwards tells you exactly what you lost.

This maps to a well-known result in learning science — the testing effect, or retrieval practice. Actively producing information strengthens memory far more than re-reading it. It’s the same reason flashcards beat highlighting. The scratchpad just applies it to framework primitives instead of vocabulary.

The key design decisions

A few things make this work that weren’t obvious when I started:

Files must be standalone. Every file runs from a fresh venv with its own requirements.txt. No shared config, no environment variables beyond what’s declared at the top. The friction of “wait, I need to set up X first” kills the ritual.

Files must be small. Under 80 lines of actual code. If it’s longer, it’s not a primitive anymore — it’s a project. A primitive you can hold in your head is a primitive you can re-derive.

Comments explain the why, not the what. You already know what Depends does syntactically. The comment should remind you when and why to reach for it versus alternatives. This is the scaffolding that reattaches the primitive to real decisions.

Tune to your actual stack. Every file in my scratchpad is pinned to decisions Cognoir actually made: 1024-dim Voyage embeddings, multi-tenant via tenant_id in metadata, custom JWT over Supabase Auth, Anthropic models, access token in React context (not localStorage). So re-grounding on the primitive simultaneously re-grounds me on the project’s architectural choices.

The ritual is non-negotiable. A scratchpad you don’t touch is an archive. An archive is worthless. The Makefile exists so that “touch a file this week” is literally one command.

When to delete files

The scratchpad is a sharpening tool, not a comprehensive reference. When I stop using a pattern in real projects, the corresponding file gets archived. If I drop LangChain for some piece and go direct-to-Anthropic SDK, the LangChain files for that pattern become dead weight.

A small scratchpad you actually use beats a comprehensive one you don’t. When in doubt, delete.

The signal that it’s working

You’ll know it’s doing its job when:

  • You stop opening FastAPI docs to check Depends syntax.
  • You can sketch a RAG pipeline on a whiteboard without pausing.
  • When your AI assistant suggests something framework-y, you can tell if the suggestion is idiomatic or cargo-culted.
  • A weird bug in your main project gets diagnosed in 5 minutes instead of 30, because the primitive is fresh.

The first sign I knew it was working: a Cognoir issue that would have sent me to the FastAPI docs a month earlier took me 3 minutes to diagnose, because I’d re-derived the exact primitive two weeks prior.

A caveat

This approach is specifically for the problem of “I know this well enough but it’s getting fuzzy.” If you’re learning a framework for the first time, this isn’t the move — you need a structured intro, tutorials, someone else’s well-documented project. The scratchpad presupposes you already understand the primitives well enough to have forgotten them.

The gap it closes is the one that opens up AFTER you’ve built something real, when project abstractions start hiding the framework beneath them. It’s a maintenance tool for fluency, not a learning tool.

Starter structure

If you want to build your own, here’s the minimum:

fundamentals/
├── README.md              # philosophy + cadence
├── CADENCE.md             # the ritual, in detail
├── Makefile               # friction killer
├── fastapi/               # one file per primitive
│   ├── 01_deps.py
│   ├── 02_auth_jwt.py
│   ├── ...
│   └── requirements.txt
├── langchain/
│   └── ...
└── (one folder per framework you care about)

Start with five files, not fifteen. Add more only when you hit a real fuzzy spot. Don’t build it all at once — it’ll bias toward completeness over usefulness.

The file list for my scratchpad, if it helps as a reference:

  • FastAPI: dependencies, custom JWT auth, BackgroundTasks vs Celery, lifespan events, Pydantic v2 idioms
  • LangChain: LCEL and Runnables, RunnableParallel/Passthrough/Lambda, structured output, streaming
  • RAG: pgvector with raw SQL, Voyage rerank, chunking strategies
  • Infra: minimal Celery, async SQLAlchemy
  • Auth: JWT primitives without a framework, bcrypt and argon2

Each file, under 80 lines, with a re-derivation prompt at the bottom. That’s the whole system.


If you try this approach and it works or doesn’t, I’d like to hear about it. Specifically interested in whether the cadence holds up past the first month — that’s where my previous attempts at similar folders fell apart, and where this one feels different.