· Programming · 6 min read
Comparing Python and TypeScript for a REST API
Explore the differences between Python and TypeScript using FastAPI and Express.js
This comparison examines two single-file implementations of a full-featured CRUD API with Discord OAuth2 authentication, Redis caching, and SQLite persistence.
The Python/FastAPI version leverages Pydantic models for automatic validation, minimal boilerplate for routing and serialization, and hides much of the async event loop behind async def
functions.
In contrast, the TypeScript/Express version enforces explicit type annotations, uses decorator-based validation with class-validator, and requires manual await
calls and middleware wiring.
I’ll explore how these differences manifest in setup, data modeling, asynchronous patterns, database/cache integration, authentication flows, error handling, performance considerations, type systems, IDE support, and ecosystem maturity.
1. Setup & Dependencies
Python + FastAPI
# install dependencies
pip install fastapi uvicorn redis python-jose[cryptography] httpx python-multipart
# run server
uvicorn main:app --reload
FastAPI bundles routing, OpenAPI documentation, and validation out of the box, so only a few imports are needed:
import json
import sqlite3
import hashlib
import os
from datetime import datetime, timedelta, timezone
from typing import List, Optional, Dict, Any
from urllib.parse import urlencode
import redis
import httpx
from fastapi import FastAPI, HTTPException, Depends
from fastapi.security import OAuth2AuthorizationCodeBearer
from fastapi.responses import RedirectResponse
from jose import JWTError, jwt
from pydantic import BaseModel, Field
TypeScript + Express
# install runtime dependencies
npm install express axios class-validator dotenv jsonwebtoken redis sqlite sqlite3 reflect-metadata
# install dev dependencies
npm install --save-dev typescript ts-node @types/node @types/express @types/jsonwebtoken @types/redis
Express requires manual setup of the HTTP server, SQLite client, Redis client, validation decorators, and middleware:
import express, { Request, Response, NextFunction } from 'express';
import { open } from 'sqlite';
import sqlite3 from 'sqlite3';
import { createClient, RedisClientType } from 'redis';
import axios from 'axios';
import jwt, { JwtPayload } from 'jsonwebtoken';
import crypto from 'crypto';
import { URLSearchParams } from 'url';
import dotenv from 'dotenv';
import 'reflect-metadata';
import { validateOrReject, IsNotEmpty, Length } from 'class-validator';
2. Models & Validation
Python (Pydantic)
class Item(BaseModel):
id: Optional[int] = Field(default=None)
name: str = Field(..., min_length=1, max_length=100)
description: str = Field(..., min_length=1, max_length=255)
created_at: Optional[str] = None
updated_at: Optional[str] = None
Pydantic auto-validates types and constraints at runtime and integrates with FastAPI to return structured 422 errors on failure.
TypeScript (class-validator)
class Item {
id?: number;
@IsNotEmpty()
@Length(1, 100)
name!: string;
@IsNotEmpty()
@Length(1, 255)
description!: string;
created_at?: string;
updated_at?: string;
}
// inside route handler:
const item = Object.assign(new Item(), req.body);
await validateOrReject(item);
class-validator uses reflect-metadata to apply decorators and throws exceptions on invalid data, requiring an explicit await
call.
3. Async Patterns & Concurrency
Python
@app.post("/items/")
async def create_item(item: Item):
with get_db_conn() as conn:
cur = conn.execute(
"INSERT INTO items (name, description) VALUES (?, ?)",
(item.name, item.description),
)
conn.commit()
return item
FastAPI’s async handlers can invoke blocking I/O synchronously (e.g., sqlite3, redis) because it manages the event loop and thread pool under the hood. This reduces visible await
usage but can hide performance bottlenecks if blocking calls are not offloaded properly.
TypeScript
app.post('/items', async (req, res) => {
const db = await getDb();
const result = await db.run('INSERT INTO items (name,description) VALUES (?,?)', item.name, item.description);
res.status(201).json({ id: result.lastID, ...item });
});
In Express, every asynchronous operation (DB, HTTP, Redis) returns a Promise, and you must await
each call. This explicit marking clarifies where code yields control but increases boilerplate and risk of forgetting an await
.
4. Database & Caching
Python
def get_db_conn():
conn = sqlite3.connect(DB_PATH, detect_types=sqlite3.PARSE_DECLTYPES)
conn.row_factory = dict_factory
return conn
if USE_REDIS:
cached = rdb.get(key)
if cached:
return json.loads(cached)
Python’s blocking Redis client simplifies calls (no await
needed), but developers must be aware that these calls run in the main thread unless configured otherwise.
TypeScript
async function getDb() {
return open({ filename: DB_PATH, driver: sqlite3.Database });
}
(async () => {
await redisClient.connect();
})();
app.get('/items/:id', async (req, res) => {
const key = `item:${req.params.id}`;
const cached = await redisClient.get(key);
if (cached) return res.json(JSON.parse(cached));
const db = await getDb();
const item = await db.get('SELECT * FROM items WHERE id = ?', req.params.id);
await redisClient.setEx(key, CACHE_TTL, JSON.stringify(item));
res.json(item);
});
TypeScript requires explicit connection logic and await
for every cache or DB call, making connection errors visible at startup rather than hidden behind implicit behavior.
5. Authentication Flows
Python
oauth2_scheme = OAuth2AuthorizationCodeBearer(
authorizationUrl="https://discord.com/api/oauth2/authorize",
tokenUrl="https://discord.com/api/oauth2/token",
)
@app.get("/auth/login")
async def login():
return RedirectResponse(auth_url)
@app.get("/auth/callback")
async def auth_callback(code: str):
token_data = httpx.post(...).json()
discord_user = httpx.get(...).json()
with get_db_conn() as conn:
conn.execute(... upsert ...)
return Token(...)
FastAPI’s Depends
and built‑in security utilities reduce boilerplate for common OAuth2 patterns and integrate with automatic docs.
TypeScript
async function getCurrentUser(req, res, next) {
const auth = req.headers.authorization?.slice(7);
if (!auth) return res.sendStatus(401);
const payload = jwt.verify(auth, JWT_SECRET_KEY) as JwtPayload;
const db = await getDb();
const user = await db.get('SELECT * FROM users WHERE discord_id = ?', payload.sub);
if (!user) return res.sendStatus(401);
req.currentUser = user;
next();
app.get('/auth/login', (req, res) => { ... });
app.get('/auth/callback', async (req, res) => { ... });
Manual middleware gives full control over flow but requires more wiring and careful error handling.
6. Error Handling & Logging
Python
@app.put("/items/{item_id}", status_code=204)
async def update_item(item_id: int):
try:
# update logic
except Exception as e:
raise HTTPException(500, detail=str(e))
Raising HTTPException
integrates directly with FastAPI’s error handlers and OpenAPI schemas.
TypeScript
app.put('/items/:id', async (req, res) => {
try {
// update logic
res.sendStatus(204);
} catch (err: any) {
console.error(err);
res.status(500).json({ detail: err.message });
}
});
Explicit res.status()
calls make error responses transparent but require repetition in each handler.
7. Performance Considerations
Independent benchmarks show FastAPI under Uvicorn ranks among the fastest Python frameworks, often handling thousands of requests per second, though it still trails pure Node.js setups by 10–20% in JSON throughput tests. Express.js, optimized by the V8 engine and native HTTP parser, frequently outperforms FastAPI in microbenchmarks but may need additional middleware tuning in production environments.
8. Type Systems & Developer Ergonomics
- Python uses dynamic typing with optional type hints provided by the
typing
module. While Pydantic enforces validation at runtime, static analysis depends on tools like MyPy or Pyright, and third-party library support for annotations remains uneven (35%–50% coverage on average). - TypeScript offers built-in static typing, catching errors at compile time and providing superior IDE integration (IntelliSense, refactoring, jump-to-definition) without extra configuration, thanks to ubiquitous
.d.ts
definitions and the TS language service.
9. IDE Support & Tooling
- Python: VS Code with Pyright delivers near real-time type checking and autocomplete on annotated code, but PyCharm’s inspections can lag and require plugins for best results.
- TypeScript: Editors like VS Code and WebStorm use the TypeScript Language Service to offer instant feedback on types, errors, imports, and refactors, even in large monorepos, making the developer experience noticeably smoother.
10. Ecosystem & Maturity
- Python: Rich libraries for data science (Pandas, NumPy), multiple mature ORMs (SQLAlchemy, Django ORM), and broad adoption in scripting and ML contexts.
- TypeScript: Seamless full-stack sharing of types with front-end frameworks (React, Angular, Vue), extensive middleware for Express, and first-class support on serverless platforms.
Conclusion
Both FastAPI and Express/TypeScript can implement a complete OAuth2 CRUD API in a single file with comparable LOC. FastAPI excels at rapid development with minimal ceremony, built-in validation, and automated docs, though its implicit async model can obscure concurrency issues. TypeScript/Express demands more boilerplate through explicit awaits, validation decorators, and middleware, but it offers end-to-end type safety, superior IDE support, and a unified language stack for full-stack teams. Choose Python for quick prototypes and data-driven tasks, and TypeScript for robust, scalable applications with consistent tooling across the stack.