SassTurf
BlogBuilding
Building

How to Build a Python API Without Losing a Week (2026)

Building a Python API for your SaaS — FastAPI vs Flask vs Django REST, auth, deployment, and the fastest path to a production endpoint.

Shubham Soni
Shubham Soni
Jul 13, 2026 · 10 min read
Table of contents9 sections
  1. 01The Endpoint Took an Afternoon. The Auth Took a Week.
  2. 02Which Framework for an API (The 30-Second Version)
  3. 03Your First Real Endpoint
  4. 04Pydantic: The Part That Saves You From Yourself
  5. 05Auto Docs: The Free Win Nobody Believes
  6. 06Auth: Start Boring, Don’t Roll Your Own Crypto
  7. 07Structuring It Before It Becomes a Swamp
  8. 08Where This Thing Actually Lives (Not Vercel)
  9. 09The Bottom Line

The Endpoint Took an Afternoon. The Auth Took a Week.

Late 2024. I was bolting an AI feature onto Clickly, my URL shortener — feed it a batch of click data, get back a “best time to share” number. I’d already lost one weekend to which Python framework? (I wrote that whole fight up in the Python web framework guide — the short version is FastAPI, and I’ll get to why in a second.) So I figured the actual API was the easy part.

It half was. I had a working endpoint returning JSON in an afternoon. I felt like a genius for about a day.

Then the questions started. How does anything outside my machine call this safely? What happens when someone POSTs garbage — a string where I wanted a number, a missing field, a nested object I never planned for? How do I stop the whole internet from hammering an endpoint that costs me money every time an LLM wakes up? How does a future me, or a customer, even know what routes exist without reading my source code?

That’s the gap nobody warns you about. Building “a Python API” isn’t writing one function that returns a dict. It’s building a contract — a promise about what goes in, what comes out, who’s allowed to ask, and where it lives. This post is that contract, assembled the cheap, boring way. It’s the how-to. If you’re still arguing about the framework, go settle that first; if you want the whole toolchain around it — ORM, migrations, packaging with uv, background jobs — that’s the cheap Python stack. This is just the API.


Which Framework for an API (The 30-Second Version)

I’m not re-litigating this — the framework post is the long argument. For building an API specifically, here’s the whole decision:

  • FastAPI is the default. It’s async-first, it validates your data from type hints, and it generates interactive docs for free. For anything talking to an AI model, a vector database, or a slow third-party service — where your API spends its life waiting — this is the one. It’s what I used for Clickly, and what most new Python APIs run on now.
  • Django REST Framework if you’re already on Django. Do not add FastAPI as a second framework next to an existing Django app just because a blog told you it’s faster. If the app is Django, expose the API with DRF and keep one mental model. It’s heavier, it’s mature, and it slots into everything Django already gave you.
  • Flask only if it’s genuinely tiny — a single webhook receiver, a five-route glue service. For new work beyond that, FastAPI just fits 2026 better.

The rest of this post assumes FastAPI, because that’s where a broke founder building a fresh API should start. Most of the ideas — contracts, auth, docs — carry over to DRF anyway.

One thing that scares people off FastAPI: it’s still a 0.x release (0.139 as I write this in mid-2026, no grand 1.0 in sight). Ignore that. It runs in production at serious companies. The version number is a naming choice, not a stability warning.


Your First Real Endpoint

Here’s the entire “hello, I’m an API” in FastAPI. Not pseudocode — this actually runs.

from fastapi import FastAPI

app = FastAPI()

@app.get("/health")
def health():
    return {"status": "ok"}

Two things worth saying out loud. First, @app.get("/health") — the decorator is the routing. No separate route table, no config file. The function right under it handles GET /health. Second, you return a plain Python dict and FastAPI serializes it to JSON with the right headers. That’s the whole loop.

You run it with Uvicorn, the ASGI server that actually speaks HTTP to the world:

uvicorn main:app --reload

--reload is a development-only flag — it restarts the server every time you save. Never ship with it on. That’s it. You have an API. Now let’s make it not embarrassing.


Pydantic: The Part That Saves You From Yourself

This is the single feature that made me stop writing APIs in anything else. In most languages, validating an incoming request is a joyless slog of if not isinstance(...) checks you write by hand and forget half of. FastAPI hands it to you for free through Pydantic.

You describe the shape of your data as a class, and that class becomes the validation:

from pydantic import BaseModel

class ShortenRequest(BaseModel):
    url: str
    expires_in_days: int = 30

@app.post("/links")
def create_link(payload: ShortenRequest):
    return {"short": make_short(payload.url), "ttl": payload.expires_in_days}

Now watch what you didn’t have to write. Someone POSTs {"url": 42}? They get a clean 422 error explaining exactly which field was wrong, before your function ever runs. They forget expires_in_days? It defaults to 30. They send "expires_in_days": "soon"? Rejected. You wrote a class; you got a bouncer.

The same trick works in reverse for responses. Declare a response_model and FastAPI guarantees your API never accidentally leaks a field — a password hash, an internal ID — because anything not in the model gets stripped on the way out. Your input is a contract and your output is a contract, and Pydantic enforces both without you writing a single manual check. For an over-engineer like me, this is the rare case where the framework does the paranoid thing so I don’t have to.


Auto Docs: The Free Win Nobody Believes

Here’s the part that still feels like cheating. Because you described everything with types and Pydantic models, FastAPI already knows your entire API — every route, every field, every response shape. So it generates an OpenAPI schema (the industry-standard machine-readable description of an API) automatically, and serves you two interactive doc pages with zero extra work:

  • /docsSwagger UI. A live page listing every endpoint where you can fill in parameters and fire real requests from the browser.
  • /redocReDoc. The same information in a cleaner, read-first, three-pane layout.

You built the docs by writing the code. That’s the whole point of OpenAPI: the description isn’t a separate document that rots the moment you change an endpoint — it’s generated from the code, so it can’t drift.

If those docs are going to be customer-facing — a public API, a partner integration, an SDK landing page — it’s worth knowing about Scalar, a newer OpenAPI renderer that looks like a modern developer tool (dark mode, built-in testing, multi-language snippets). It got picked as the default in .NET 9, which tells you something. Because Swagger, ReDoc, and Scalar are all just renderers of the same OpenAPI schema, swapping between them is a one-line change — you’re never locked in. For an internal API, don’t bother; /docs is already better than 90% of the hand-written documentation on the internet.


Auth: Start Boring, Don’t Roll Your Own Crypto

This is where I lost my week, and where I watch other founders lose theirs. There are three levels, and the mistake is always reaching for the fanciest one first.

Level 1 — API keys. If your API is machine-to-machine — a backend service, a cron job, your own frontend calling your own backend — a long random API key in a header is often all you need. You generate a key, store its hash in Postgres, and check every incoming request against it. That’s it. Yes, API keys are “less secure” than the alternatives in the sense that a leaked key is a leaked key — so rotate them, never log them, and always run over HTTPS. But for a solo founder’s first API, this is the right amount of security, and you can ship it today.

Level 2 — JWT. The moment real users log in and you want stateless sessions, you graduate to JSON Web Tokens. The flow: a user sends credentials once, you verify the password (hashed with bcrypt, never stored in plain text — this is non-negotiable), and you hand back a signed token. Every later request carries that token in the Authorization header, and you verify the signature without touching the database. FastAPI gives you the OAuth2 primitives for exactly this, and the actual token work goes through PyJWT.

But here’s the trap, and I’m going to be blunt because I fell in it: do not free-hand your own JWT logic at 1 AM. The failure modes are subtle and they’re security holes, not bugs. Pin a current PyJWT, always specify the signing algorithm explicitly (a whitelist, so an attacker can’t downgrade you to “none”), and keep access tokens short-lived. If security work makes your eyes glaze over — and for most of us it should — this is the one place where paying for a managed auth provider, or leaning on your database provider’s built-in auth, is money well spent. I said the same thing in the stack post and I’ll keep saying it: rolling your own auth is the classic over-engineering move that quietly sinks projects.

Level 3 — full OAuth2 with third parties. “Log in with Google,” scopes, refresh-token rotation. Real, but almost certainly not your first-week problem. Don’t build it until a customer asks.

My honest path: ship with an API key. Add JWT when humans log in. Reach for full OAuth only when someone’s paying you enough to demand it.


Structuring It Before It Becomes a Swamp

One 400-line main.py is fine for a weekend. It is not fine by the time you have twenty endpoints, and the migration is annoying, so do it early. FastAPI’s answer is APIRouter — you split routes into files by feature (links.py, users.py, billing.py), each with its own router, and mount them onto the main app. Same idea DRF gives you with viewsets and routers.

The layout I settle into, kept deliberately dull:

  • main.py — creates the app, mounts the routers, nothing else.
  • routers/ — one file per resource. This is where your endpoints live.
  • models.py — the Pydantic contracts (request and response shapes).
  • db.py — the database connection and, if you’re on FastAPI, your SQLAlchemy setup.

That’s the whole architecture for a small SaaS API. Resist the urge to add “clean architecture” layers with four folders of abstractions between the request and the database. You have no users yet. A dependency-injected repository pattern is a beautiful thing to build at 2 AM and a terrible thing to maintain when you’re also doing sales, support, and marketing. Boring wins.


Where This Thing Actually Lives (Not Vercel)

You’ve got a working, documented, authenticated API. Now the question a broke founder cares about most: where does it run without a surprise bill?

Not Vercel. I love Vercel for JavaScript, and yes, it technically runs Python — but only as serverless functions. A real Python API is a long-running process. The second it needs a persistent connection, a background worker, a WebSocket, or a request that outlives a serverless timeout because an LLM is still thinking, Vercel fights you. Python is a guest there, not a citizen.

You want a container-based platform — the same trio from my deployment tier list:

  • Railway — my default. Push the repo, it detects Python, you get a flat monthly bill instead of per-request roulette. Start here.
  • Fly.io — brilliant if you want your API in containers close to your users, with Postgres and workers alongside it. CLI-heavy, so not for total beginners.
  • Koyeb — natively deploys FastAPI, Flask, and Django on a real always-on server for a flat fee, zero per-request anxiety.

On any of them you run the app under Uvicorn (often behind a process manager that spins up a few workers), point it at your Neon Postgres, set your secrets as environment variables, and you’re live. The clean split if you’re a JavaScript person adding a Python API: keep the frontend on Vercel, put the Python API on Railway or Fly, and call it over HTTP. That’s the exact same pattern I use for splitting a Node API off Vercel — the language changes, the wiring doesn’t, and the full deployment guide walks the whole split with billing caps set.


The Bottom Line

I lost a week to a Python API because I treated it like a codebase problem when it was a contract problem. Get the contract right and the code is small. Here’s the whole thing, no ceremony:

FastAPI for a new API, DRF if you’re already on Django, Flask only when it’s tiny. Describe every request and response with Pydantic and let it do the validation you’d otherwise forget. Let the framework generate your OpenAPI docs — Swagger for internal, Scalar if customers will read them. Start auth with a boring API key, graduate to JWT when humans log in, and for the love of your users, don’t hand-roll the crypto. Structure it into routers before it becomes a swamp. Then deploy the long-running process on Railway, Fly, or Koyeb — never on Vercel’s serverless functions.

The API was never the hard part. The contract was. Get that right and you’ll ship the endpoint this afternoon, like I finally did — just without the week I threw away learning it the expensive way.

This is the Broken Engineer Guide — I over-engineer everything, fail at business, and hand you the shortcuts so you don’t have to bleed for them. Now uvicorn main:app and ship the thing.

Shubham Soni
Written by
Shubham Soni

A decade building, launching, and occasionally breaking SaaS products. I write SassTurf to share what actually moved the needle — free, no fluff.

Keep reading

Building

LangChain Alternatives That Actually Earn Their Place (2026)

9 min read
Building

Kubernetes Deployment: A Founder's Guide to the One File You'd Actually Write (2026)

9 min read
Email Marketing

Out of Office Email Templates That Don't Sound Like a Robot (2026)

8 min read

Enjoyed this? Get the next one.

One useful SaaS essay in your inbox each week. No fluff, unsubscribe anytime.