← Back to blogBackend

FastAPI for a Node.js developer: the Python backend I didn't expect to enjoy

October 14, 2025·7 min read

I've written Node.js backends for most of my career. TypeScript with Express or NestJS is my default stack. When a project required a Python backend, I expected to tolerate it. Instead, I found FastAPI genuinely enjoyable.

The project was an internal data processing service. The data science team had existing Python code for validation, transformation, and ML inference. Wrapping that code in a Node.js service via child processes or Python microservices was possible but added complexity. Writing the API layer in Python alongside the existing Python code was simpler.

Why FastAPI over Django REST Framework

Django REST Framework (DRF) is the established choice for Python APIs. It's mature, well-documented, and used by large companies. But for API-first services, it carries unnecessary weight.

DRF comes with Django, which includes an ORM, a template engine, an admin interface, session management, and middleware designed for server-rendered web applications. For a service that only serves JSON responses, most of Django is unused overhead.

FastAPI is designed specifically for building APIs:

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class EventInput(BaseModel):
    event_type: str
    payload: dict
    timestamp: float

class EventResult(BaseModel):
    valid: bool
    processed_at: str
    warnings: list[str] = []

@app.post("/events/validate", response_model=EventResult)
async def validate_event(event: EventInput) -> EventResult:
    warnings = run_validation(event)
    return EventResult(
        valid=len(warnings) == 0,
        processed_at=datetime.now().isoformat(),
        warnings=warnings,
    )

The route handler declares its input type (EventInput) and output type (EventResult). FastAPI validates the incoming request against the Pydantic model automatically. If validation fails, it returns a 422 with specific error messages. No validation middleware, no manual checks.

Pydantic vs Zod

Pydantic is Python's equivalent of Zod in TypeScript. Both define schemas for data validation. The comparison:

# Pydantic
from pydantic import BaseModel, Field, validator

class User(BaseModel):
    name: str = Field(min_length=1, max_length=100)
    email: str
    age: int = Field(ge=0, le=150)
    role: str = "user"

    @validator("email")
    def email_must_be_valid(cls, v):
        if "@" not in v:
            raise ValueError("Invalid email")
        return v.lower()
// Zod
const UserSchema = z.object({
  name: z.string().min(1).max(100),
  email: z.string().email().transform(v => v.toLowerCase()),
  age: z.number().int().min(0).max(150),
  role: z.string().default("user"),
});

Pydantic models are classes. Zod schemas are objects. Pydantic uses Python type annotations and decorators. Zod uses method chaining. Both provide compile-time type information and runtime validation.

The practical difference: Pydantic models are also the ORM-like data containers. A Pydantic model instance has attributes you can access with dot notation. A Zod schema's output is a plain object. For Python code that passes data between functions, Pydantic's model instances are more ergonomic.

Async support

FastAPI supports async natively:

@app.get("/sessions/{session_id}")
async def get_session(session_id: str):
    session = await db.sessions.find_one({"id": session_id})
    if not session:
        raise HTTPException(status_code=404, detail="Session not found")
    return session

The async model works similarly to Node.js: async functions yield control during I/O operations. Under the hood, FastAPI uses uvicorn with asyncio, Python's event loop.

The comparison with Node.js:

  • Node.js: single-threaded, all I/O is async by default, CPU-bound work blocks the event loop
  • Python/FastAPI: async for I/O-bound endpoints, but you can also define sync endpoints that run in a thread pool automatically
# Sync endpoint - FastAPI runs this in a thread pool
@app.post("/process")
def process_data(data: DataInput):
    result = heavy_computation(data)  # CPU-bound, runs in thread
    return result

# Async endpoint - runs on the event loop
@app.get("/status")
async def get_status():
    status = await check_service_health()
    return status

This is a genuine advantage over Node.js. In Node.js, CPU-bound work requires explicit worker threads. In FastAPI, sync endpoints automatically use threads without any additional code.

Automatic OpenAPI documentation

FastAPI generates OpenAPI (Swagger) documentation from the route definitions and Pydantic models:

app = FastAPI(
    title="Event Processing API",
    description="Validates and processes incoming events",
    version="1.0.0",
)

Navigate to /docs and you get an interactive Swagger UI. Navigate to /redoc and you get a ReDoc interface. Both are generated automatically from the type annotations and docstrings. No additional plugins, no separate documentation files.

The interactive Swagger UI lets you test endpoints directly from the browser. For internal services used by other engineers, this replaces the need for a separate API documentation effort.

Where Python's ecosystem wins

The reason I used Python for this project was the ecosystem for data manipulation and ML:

  • pandas for data transformation: reading CSVs, joining datasets, aggregating, filtering. The equivalent in Node.js requires multiple libraries and more code.
  • scikit-learn for ML inference: loading a trained model and running predictions is a few lines of code. The Node.js ML ecosystem is less mature.
  • numpy for numerical computation: vectorized operations on arrays are orders of magnitude faster than JavaScript loops.

For the event processing service, the validation rules included statistical checks (is this value within three standard deviations of the historical mean?) and ML-based anomaly detection. Writing these in Python with pandas and scikit-learn took a day. Writing equivalent code in Node.js would have taken a week and produced slower results.

What I miss from Node.js

TypeScript's type system. Python's type annotations with mypy are good but not as comprehensive as TypeScript's type system. Union types, discriminated unions, conditional types: TypeScript handles these more elegantly.

npm ecosystem for web tooling. Node.js has more libraries for web-specific tasks: JWT handling, rate limiting, WebSocket management. Python has equivalents, but the Node.js options are generally more mature.

Hot reloading speed. uvicorn --reload restarts the server on file changes. It's fast but not as fast as Node.js's hot reloading. For a service with a large import chain, restarts can take 2-3 seconds.

Deployment simplicity with Docker. Python dependencies are harder to containerize than Node.js dependencies. pip install can involve compiling C extensions (numpy, pandas), which requires build tools in the Docker image. The equivalent Node.js dependencies usually have pre-built binaries.

When to reach for FastAPI

FastAPI is the right choice when:

  • The existing codebase or team expertise is Python-first
  • The service involves data processing, ML, or scientific computing
  • You want automatic API documentation without additional tooling
  • The service has a mix of I/O-bound and CPU-bound endpoints

Node.js with TypeScript remains my default for web APIs, real-time services, and projects where the frontend team also works on the backend. FastAPI is the right tool when Python's ecosystem is genuinely the better fit, not a concession to circumstance.

RESPONSES

Leave a response