FastAPI for TypeScript Developers

Python DX now rivals TypeScript. Here's how FastAPI, uv, and Pydantic make Python feel modern, with type safety and performance that matches what you're used to.


FastAPI for TypeScript Developers

TL;DR Over the last several years, tools like uv, Pydantic, FastAPI, and type hints have made Python DX feel as modern as TypeScript, with similar performance and type safety.

I started my career as a JavaScript developer in 2014, but I ended up working primarily in Python from 2018–2021. Now I’m back to JS (well, TypeScript this time around), and I’ve started to miss Python.

When I transitioned back to the JS ecosystem, I was reminded how amazing JS tooling is. npm (or pnpm, or bun) felt incredibly modern compared to pip. Python DX seemed dated; JS felt like coding in 2050. Now that I’m dabbling in Python again, I can confidently say things have changed.

It all starts with uv. uv is a package manager written in Rust that makes Python feel as modern as TypeScript. Using uv feels like using bun as your toolchain; in other words, it’s fast and easy to use. The creators of uv also released ruff, a Python linter and formatter. ruff is like ESLint and Prettier rolled into one. Together, uv and ruff provide a DX comparable to the JS ecosystem. Add Python’s type hints along with a type checker like mypy or pyright, and you have the safety of types, IDE autocomplete, and all the other niceties that tools like TypeScript provide. All this is to say, Python now has modern tooling and can reach type-safety.

Python’s async capabilities have also matured significantly, and new frameworks have taken full advantage. FastAPI is a standout (Litestar is another standout that I will write about soon). Combining uv, ruff, type hints, and FastAPI provides a type-safe, high-performance backend with TypeScript-level DX. And when I say “high-performance,” I mean it: FastAPI is one of the fastest Python frameworks available, on par with Node.js and Go.

Conceptual Parallels (Familiar Ground)

If you’re coming from NestJS or even just modern Express/Fastify with TypeScript, you’ll find that many concepts map 1:1.

  • async/await: Nearly identical syntax and mental model.
  • Decorators: @app.get("/users") feels like NestJS @Get('users').
  • Dependency injection: FastAPI’s Depends() is lighter than NestJS DI but follows the same idea, using functions instead of classes.
  • Union types: str | int vs string | number.
  • Optional/nullable: str | None vs string | undefined.
  • Generics: Same concept, slightly different syntax (list[T] vs Array<T>).

Tooling

TypeScriptPython
tscmypy / pyright
eslint + prettierruff (does both)
jest / vitestpytest
npm / pnpmuv
package.jsonpyproject.toml
node_modules.venv
tsconfig.jsonpyproject.toml
fetch / axioshttpx

Package Management: Use uv

If you try pip first, you’ll feel like you’ve traveled back to 2010. No lockfile, no project-scoped installs by default, manual virtual environment management. It’s painful coming from npm/pnpm.

uv fixes this. It’s what makes Python feel modern:

  • uv init → like npm init
  • uv add fastapi → like npm install fastapi
  • uv run main.py → runs in the project’s virtual environment automatically
  • Lockfile (uv.lock) generated by default
  • Written in Rust so it’s very fast (faster than pnpm even)
  • Single tool replaces pip, pip-tools, virtualenv, and pyenv
  • uv is more similar to using bun as your toolchain rather than npm or pnpm

The workflow you want:

uv init my-api
cd my-api
uv add fastapi --extra standard
uv run fastapi dev src/main.py --reload

This is the closest thing to the npm/pnpm experience you’ll find in Python. Skip pip, skip poetry, start with uv.

Code Examples: Side-by-Side Comparisons

Route Handlers

Here is how a basic route looks in Express vs. FastAPI. Notice how FastAPI uses type hints to automatically parse and validate the user_id as an integer.

Express (TypeScript)

app.get("/users/:id", (req, res) => {
  const userId = parseInt(req.params.id);
  res.json({ user_id: userId, name: "John Doe" });
});

FastAPI

@app.get("/users/{user_id}")
async def read_user(user_id: int):
    return {"user_id": user_id, "name": "John Doe"}

Pydantic Models vs. Zod Schemas

FastAPI makes liberal use of another Python package: Pydantic. Pydantic is similar to Zod or Valibot, but with built-in serialization in addition to data validation. Pydantic and its required type information make data validation a breeze.

Zod (TypeScript)

import { z } from "zod";

const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email().optional(),
});

type User = z.infer<typeof UserSchema>;

Pydantic (Python)

from pydantic import BaseModel, EmailStr

class User(BaseModel):
    id: int
    name: str
    email: EmailStr | None = None

One major advantage: in Pydantic, the class is both the schema and the type. You don’t need to “infer” anything; you just use User everywhere.

End-to-End Type Safety

Type hints and Pydantic allow for automatically generating an OpenAPI spec. This gives you swagger docs for free, but even better it’s allows for end-to-end type safety.

By using a tool like Hey API, you can turn your backend models into a TypeScript SDK with one command:

npx @hey-api/openapi-ts -i http://localhost:8000/openapi.json -o src/client

This eliminates a whole class of bugs. If you change a field name in your Pydantic model and regenerate your SDK, your TypeScript compiler will immediately flag the mismatch in your frontend. You get full autocompletion for method names, parameters, and response objects, ensuring your API description stays perfectly in sync with your actual code.

For larger applications, you can even customize how operation IDs are generated to ensure your SDK method names are clean and intuitive (e.g., ItemsService.get_items() instead of a generated name like get_items_items_get).

Dependency Injection

FastAPI has built-in DI that feels similar to NestJS but lighter. Instead of complex class-based providers, you use Depends().

NestJS

@Injectable()
export class DatabaseService {
  private db: Database;

  async onModuleInit() {
    this.db = new Database();
  }

  async onModuleDestroy() {
    await this.db.close();
  }

  queryItems() {
    return this.db.query("SELECT * FROM items");
  }
}

@Controller("items")
export class ItemsController {
  constructor(private readonly dbService: DatabaseService) {}

  @Get()
  async readItems() {
    return this.dbService.queryItems();
  }
}

FastAPI

async def get_db():
    db = Database()
    try:
        yield db
    finally:
        db.close()

@app.get("/items/")
async def read_items(db: Database = Depends(get_db)):
    return db.query_items()

NestJS requires defining an injectable class, registering it in a module, and using constructor injection. FastAPI’s approach is more functional: you define a generator function and declare the dependency inline. Both achieve the same result, but FastAPI’s version is about 1/3 the code.

The async/await Story

Python’s async syntax is nearly identical to JS. FastAPI handles both async and regular def (sync) handlers, but for I/O bound tasks, async is the way to go.

Concurrent Requests

Python’s equivalent to fetch is httpx. You’ll see requests mentioned everywhere as Python’s go-to HTTP library, but it’s syncronous. httpx has a nearly identical API but supports async, making it the right choice for FastAPI.

JavaScript (Promise.all)

const requestPromises = urls.map((url) => fetch(url));
const results = await Promise.allSettled(requestPromises);

Python (asyncio.gather + httpx)

import asyncio
import httpx

async with httpx.AsyncClient() as client:
    requests = [client.get(url) for url in urls]
    results = await asyncio.gather(*requests, return_exceptions=True)

Both patterns make multiple async requests concurrently and return both successes and errors in the results.

IDE Experience / Type Inference

A common misconception is that Python is “untyped.” With Pylance/Pyright in VS Code, the autocomplete and type checking rivals TypeScript.

  • Type checking is a separate tool (mypy/pyright), not the runtime.
  • No compilation step: types are hints, but Pydantic enforces them at the boundaries of your application (request bodies, query params).

Testing

FastAPI’s testing story is straightforward if you’re coming from Jest or Vitest. You’ll use pytest as your test runner and FastAPI’s built-in TestClient (powered by httpx) to make requests against your app.

Jest + Supertest (TypeScript)

import request from "supertest";
import { app } from "./app";

describe("GET /users/:id", () => {
  it("returns a user", async () => {
    const response = await request(app).get("/users/1");
    expect(response.status).toBe(200);
    expect(response.body.name).toBe("John Doe");
  });
});

pytest + TestClient (Python)

import pytest
from fastapi.testclient import TestClient
from app import app

@pytest.fixture
def client():
    return TestClient(app)

def test_read_user(client):
    response = client.get("/users/1")
    assert response.status_code == 200
    assert response.json()["name"] == "John Doe"

The mental model is nearly identical: create a test client, make requests, assert on responses. pytest fixtures replace beforeEach/beforeAll setup functions, and simple assert statements replace expect() matchers. Run your tests with uv run pytest and you’re done.

Mindset Shifts / Gotchas

Making the switch does require a few mindset shifts. You’ll need to get used to the path-based import system, which can feel a bit clunky compared to TS module resolution. You’ll also need to embrace virtual environments, though uv has finally made this a background task rather than a manual chore. Most importantly, remember that unlike the opinionated structure of NestJS or Django, FastAPI follows the Express/Fastify philosophy: you can start with a single file and only add structure (routers, models, services) as your project naturally grows.

ORM / Database Parallels

TypeScript EcosystemPython Ecosystem
TypeORMSQLAlchemy (heavy, full-featured)
Prisma / DrizzleSQLModel (Pydantic + SQLAlchemy)
Raw pgRaw asyncpg

What’s Actually Better (and What You’ll Miss)

While the ecosystems are similar, there are areas where Python actually takes the lead. Pydantic’s validation errors tend to be more structured and actionable than Zod’s, and having a single source of truth for validation, serialization, and documentation is a massive maintenance win. Plus, Python’s data manipulation story, whether you’re using native list comprehensions (which run at the speed of C) or powerful libraries like Pandas and Polars, is incredibly hard to beat.

Of course, it isn’t all sunshine. You might find that IDE inference isn’t quite as snappy as the TypeScript server, and refactoring tools or monorepo management (compared to Turbo or Nx) aren’t quite as mature yet.

Progressive Complexity Path

One of the best things about FastAPI is how it scales with your needs:

  1. Single file, single function to start.
  2. Add Pydantic models when validation matters.
  3. Add Depends() when you need shared logic.
  4. Add routers when you need organization.
  5. Add middleware when you need cross-cutting concerns.

When to Choose FastAPI Over Node?

FastAPI makes sense when:

  • You need to integrate with AI/ML or Data Science libraries (which are overwhelmingly Python-first).
  • Your team already has Python expertise.
  • You want the best-in-class OpenAPI/Swagger experience with zero effort.
  • You’re building a data-heavy backend where Python’s ecosystem shines.

FastAPI combined with uv and ruff has brought Python back into the conversation for developers who value great DX. If you’ve been staying away from Python because of the “old way” of doing things, it’s time to take another look.