We build the same FastAPI endpoint three ways and compare trade-offs with code
Have you ever shipped something that “felt right”… and a week later you’re untangling spaghetti?
Have you ever written tests after the code and realised they’re just confirming happy paths?
Have you ever had a perfectly fine implementation… that didn’t match what Product actually meant?
If any of that sounds familiar, this post is for you. We’ll compare three very real ways engineers work in 2025:
- Vibe coding (code-first, intuition-driven)
- Test-Driven Development (TDD) (red → green → refactor)
- Spec-Driven Development (start with an executable spec; code follows)
To keep it concrete, we’ll build the same tiny feature three ways: a POST /price endpoint that adds VAT and applies a discount code. You’ll see what each mode feels like, where it shines, where it bites and you can open the matching folders in the repo (vibe/, tdd/, spec_driven/) to run the examples and go deeper.
A 60-second analogy you won’t forget
Vibe coding is like cooking by feel. You throw in garlic “until it smells right.” It’s brilliant for exploring flavours fast but without a recipe, repeating success is hard.
TDD is cooking with a scale. You weigh, taste, adjust, then refactor the plating. Fewer surprises; you can serve 20 plates consistently.
Spec-driven is a recipe card everyone agrees on before you start: ingredients, steps, expected taste. The sous-chef, the server, and you are aligned.
We’ll use all three to make the same dish so the differences pop.
The Problem: a tiny price API
We want a single endpoint:
POST /price
{ "amount": 100.0, "vat_pct": 20, "code": "WELCOME10" } -> { "final": 108.0 }
Rule: VAT is applied first, then a 10% discount for WELCOME10. Round to 2 decimals.
1) Vibe Coding “I’ll just build it”
Have you ever been in flow, built the endpoint in one sitting, and thought: tests can come later? That’s vibe coding. It’s excellent for prototypes and spikes. Risk: edge cases get discovered by users or future-you.
What it feels like, step by step
- Spin up a FastAPI app.
- Implement the “obvious” math.
- Add basic tests after it works.
- Learn from behaviour, refactor in place.
Minimal snippet (from vibe/app.py)
`# POST /price -> {"final": number}
from fastapi import FastAPI
from pydantic import BaseModel
from .calculator import final_price
app = FastAPI(title="vibe-pricing")
class PriceIn(BaseModel):
amount: float
vat_pct: float
code: str | None = None
@app.post("/price")
def price(body: PriceIn):
return {"final": final_price(body.amount, body.vat_pct, body.code)}`
Reality check (in vibe/tests/)
You’ll likely add tests that match what you already wrote:
def test_vat_then_discount():
assert final_price(100, 20, "WELCOME10") == 108.0
When it shines
Spikes, demos, “feel the API” explorations
Rapid iteration with minimal ceremony
Watch-outs
Hidden edge cases (rounding, nulls, unknown codes)
Harder to reproduce success; tests risk becoming rubber stamps
2) TDD “Red → Green → Refactor”
Have you ever wished your code told you when you broke something? TDD is that feedback loop you write a failing test (red), make it pass (green), then clean it up (refactor).
What it feels like, step by step
- Write a failing test that defines behaviour.
- Write the tiniest code to pass it.
- Refactor confidently (tests stay green).
- Repeat, growing behaviour by behaviour.
Start with a test (from tdd/tests/test_calculator.py)
`from dev_modes.tdd.calculator import price_with_vat, final_price
def test_vat_rounds_to_cents():
assert price_with_vat(19.99, 8.5) == 21.64
def test_final_price_vat_then_discount():
assert final_price(100, 20, "WELCOME10") == 108.0`
Then the code (from tdd/calculator.py)
`def price_with_vat(amount: float, vat_pct: float) -> float:
return round(amount * (1 + vat_pct / 100), 2)
def apply_discount(total: float, code: str | None) -> float:
if not code: return total
if code.lower() == "welcome10": return round(total * 0.9, 2)
return total
def final_price(amount: float, vat_pct: float, code: str | None) -> float:
with_vat = price_with_vat(amount, vat_pct)
return apply_discount(with_vat, code)`
When it shines
- Core business logic and refactor-heavy domains
- Teams that value predictability and regression safety
Watch-outs
- Over-mocking and “testing implementation not behaviour”
- You need discipline to resist jumping straight to code
3) Spec-Driven “Agree on the recipe, then cook”
Have you ever built the right thing the wrong way or the wrong thing perfectly? Spec-driven starts with a human-readable, executable spec so everyone agrees on “what good looks like” before code.
What it feels like, step by step
- Write the spec in plain language (e.g., Gherkin).
- Derive tests from the spec.
- Implement the code to satisfy the spec.
- Treat the spec as living documentation.
*Spec first (from spec_driven/specs/price.feature)
*
Feature: Checkout price
Scenario: VAT then discount
Given a base amount of 100
And VAT is 20 percent
When I apply the code "WELCOME10"
Then the final price should be 108.0
Test shaped by the spec (from spec_driven/tests/test_from_spec.py)
`from fastapi.testclient import TestClient
from dev_modes.spec_driven.app import app
client = TestClient(app)
def test_vat_then_discount_from_spec():
r = client.post("/price", json={"amount": 100, "vat_pct": 20, "code": "WELCOME10"})
assert r.status_code == 200
assert r.json() == {"final": 108.0}`
When it shines
Cross-team features (Product, QA, Eng) and compliance-heavy work
API contracts where misunderstandings are expensive
Watch-outs
A little slower to start (but faster to align)
Specs need ownership or they rot
How to choose (today)
Ask yourself three questions before you start:
Is this exploration or execution?
If you’re exploring unknowns, start with vibe for speed then tighten with TDD or a spec once the shape stabilises.
Is this change cross-team or customer-visible?
If multiple stakeholders must agree, spec-driven pays for itself in clarity.
Will we refactor this a lot?
If yes, TDD buys you courage and speed with safety.
Show me the code (and how to run it)
All three implementations build the same POST /price:
vibe/ : code first, tests after
tdd/ : tests first, then code
spec_driven/ : feature spec → tests → code
Run any folder with your FastAPI dev server and tests. For example:
# Vibe
uvicorn dev_modes.vibe.app:app --reload
pytest vibe/tests -q
# TDD
uvicorn dev_modes.tdd.app:app --reload
pytest tdd/tests -q
# Spec-driven
uvicorn dev_modes.spec_driven.app:app --reload
pytest spec_driven/tests -q
Quick “save yourself later” checklist
Vibe? Jot down two edge cases you didn’t code yet. Add a micro-test for each before merging.
TDD? Re-read your tests: do they describe behaviour, or mirror implementation? Trim mocks.
Spec-driven? Keep the spec crisp and owned. If Product changes their mind, change the spec first.
Final thought
All three modes are tools, not tribes. Great engineers switch hats on purpose. Explore with vibe, harden with TDD, align with specs and your future self (and your directors) will thank you.




Top comments (0)