The Mullet Stack

mooreds1 pts0 comments

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&rsquo;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 &ldquo;hello&rdquo; 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&rsquo;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&rsquo;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&rsquo;s nothing incoming for Pydantic<br>to reject. Send a malformed payload and you&rsquo;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&rsquo;s<br>Python&rsquo;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&rsquo;s the trade against FastAPI&rsquo;s...

item fastapi python rsquo backend items

Related Articles