The Mullet Stack
Why this exists
It is so easy to generate a ton of code these days, but we still need to<br>understand both low level syntax and higher level trade-offs between different<br>libraries and frameworks. I wanted to create a field guide to stay current on<br>modern full-stack web development with JavaScript and Python. Hence the mullet:<br>JavaScript in the front, Python in the back.
This project is not intended to be authoritative. I chose what seems like the<br>best combination of tools right now and tried to describe other options along<br>the way. My current full-time job is maintaining a Django app with a jQuery<br>front-end, so this is just me exploring and learning in public.
React and TypeScript have matured a lot in the last couple of years. Python’s<br>web frameworks have evolved too, and we will focus on FastAPI with Pydantic for<br>this project. Each of their type systems implements the same Item in a<br>slightly different way. What is the best way to bring these two technologies<br>together into one stack?
The snippets ahead are windows into the repo rather than a build-along, so the<br>fastest way to follow is to clone it, start both servers, and poke at the files<br>as you read. It is a backend that returns a list of items, and a frontend that<br>fetches it. The example code is in this repo (app/backend, app/frontend).<br>Feedback is welcome!
1. Setting up
Before either side does anything interesting, get the smallest possible version<br>of each running side by side. Nothing shared yet, no wiring, just “hello” on two<br>different ports.
Backend. Python projects declare dependencies in pyproject.toml:
# pyproject.toml<br>[project]<br>name = "mullet-backend"<br>requires-python = ">=3.12"<br>dependencies = [<br>"fastapi[standard]>=0.115",
uv sync # resolves and installs into a local .venv<br>uv run fastapi dev # serves on :8000, reloads on save
Frontend. JavaScript projects declare theirs in package.json:
"dependencies": { "react": "^19.2.8", "react-dom": "^19.2.8" },<br>"devDependencies": { "vite": "^8.2.1", "typescript": "^5.9.3", "vitest": "^4.1.10" }
npm install # resolves and installs into node_modules<br>npm run dev # serves on :5173, reloads on save
Two commands, two dev servers. Point a browser at :8000/docs and :5173 and<br>you have the front and back ends.
A packaging note. uv sync and npm install look like the same step, but<br>npm ran arbitrary code at install time via lifecycle scripts until npm 12 turned<br>that off by default in July 2026, while Python wheels never did. Worth digging<br>deeper another time to learn more.
2. Backend: FastAPI + Pydantic
We use Python on the backend to define the shape and serve a list of items:
# app/models.py<br>from pydantic import BaseModel
class Item(BaseModel):<br>id: int<br>name: str<br>description: str | None = None<br>tags: list[str] = []<br>in_stock: bool = True
# app/main.py<br>from fastapi import FastAPI<br>from app.models import Item
app = FastAPI(title="mullet-stack backend")
ITEMS = [<br>Item(id=1, name="Enamel mug", tags=["kitchen", "camping"]),<br>Item(id=2, name="Field notebook", description="Grid pages, pocket-sized", tags=["stationery"]),<br>Item(id=3, name="Multitool", tags=["hardware"], in_stock=False),
@app.get("/items", response_model=list[Item])<br>def list_items() -> list[Item]:<br>return ITEMS
uv run fastapi dev and localhost:8000/items returns a JSON array. FastAPI<br>reads the type hints on Item and the route signature then generates an<br>interactive OpenAPI page from them at localhost:8000/docs without a separate<br>schema file to keep in sync.
Which is worth pausing on, because those hints do nothing by themselves.<br>Python’s type hints are not enforced by the interpreter. Write the same<br>annotation on a plain class and nothing stops you at runtime:
class Plain:<br>def __init__(self, id: int):<br>self.id = id
Plain(id="not a number").id # 'not a number', no complaint
Hints are documentation and a hook for external tools like mypy or pyright,<br>checked before the code ever runs, not while it runs. Pydantic is what closes<br>that gap. Item carries the identical annotations, but because it’s a<br>BaseModel those annotations became a runtime contract: Item(id="not a<br>number", name="x") raises ValidationError on the spot. Same hints, same<br>syntax, completely different enforcement.
But GET /items takes no request body, so there’s nothing incoming for Pydantic<br>to reject. Send a malformed payload and you’ll get a 200, because the handler<br>never asked for input. response_model=list[Item] guards the way out: it<br>validates what the handler returns. FastAPI can help with input validation, but<br>only with endpoints that declare a request body or typed query params. That’s<br>Python’s gradual typing: annotations are always optional, and how much they do<br>depends entirely on what you bring in to enforce them.
The roads not taken: Django, Flask, Ninja
Django is the batteries-included option: if you expect an admin panel and an ORM<br>out of the box, that’s the trade against FastAPI’s...