🚀 FastAPI for Perl Developers: From Dancer to Modern Python APIs
2026-02-12
After two decades of building APIs with Perl's Dancer framework, I recently dove into FastAPI and the contrast is stark. Where Dancer gives you a lightweight canvas and says "paint," FastAPI hands you a precision-engineered paint-by-numbers kit that somehow produces gallery-worthy results. Both frameworks solve the same problem, but their philosophies couldn't be more different. 🦞🚀
The Dancer Way: Expressive Minimalism
Perl's Dancer (and Dancer2) has been my go-to for quick APIs and microservices. It's beautifully minimal:
use Dancer2;
get '/api/user/:id' => sub {
my $user_id = route_parameters->get('id');
my $user = fetch_user($user_id); # Your DB lookup
return to_json {
id => $user->id,
name => $user->name,
email => $user->email,
};
};
start;
This is quintessentially Perl: pick whatever JSON serializer you want (JSON::XS, Cpanel::JSON::XS, JSON::MaybeXS), handle your own database connections, manage validation manually and deploy via Plack. It's flexible, but you're responsible for everything.
Enter FastAPI: Batteries Included, Validation Automatic
FastAPI approaches APIs with Python's type system as the foundation:
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import Optional
app = FastAPI()
class User(BaseModel):
id: int
name: str
email: str
bio: Optional[str] = None
@app.get("/api/user/{user_id}", response_model=User)
async def get_user(user_id: int):
user = await fetch_user(user_id) # Your async DB lookup
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user
The differences are immediate and profound:
| Aspect | Dancer (Perl) | FastAPI (Python) |
|---|---|---|
| Routing | Subroutine attributes | Decorators with type hints |
| Validation | Manual, DIY | Automatic via Pydantic |
| Documentation | None built-in | Auto-generated OpenAPI/Swagger |
| Serialization | Manual JSON encoding | Automatic from type hints |
| Async | Limited, add-on | Native, built-in |
The Pydantic Revolution: Type Hints as Validation
Here's where Perl developers feel the paradigm shift. In Dancer, you'd manually validate:
post '/api/users' => sub {
my $body = from_json(request->body);
# Manual validation everywhere
unless ($body->{name} && length($body->{name}) >= 2) {
send_error("Name required, min 2 chars", 400);
}
unless ($body->{email} && $body->{email} =~ /\@/) {
send_error("Valid email required", 400);
}
my $user = create_user($body);
status 201;
return to_json({ id => $user->id });
};
FastAPI with Pydantic moves validation to the type system:
from pydantic import BaseModel, EmailStr, Field
class UserCreate(BaseModel):
name: str = Field(..., min_length=2, max_length=100)
email: EmailStr # Validates email format automatically
bio: Optional[str] = Field(None, max_length=500)
@app.post("/api/users", response_model=User, status_code=201)
async def create_user(user_data: UserCreate):
user = await create_user_in_db(user_data.dict())
return user
Invalid requests get automatic 422 responses with detailed error messages, no manual validation code required. The type system becomes your guardrails.
Dependency Injection: Dancer's Plugin System, Reimagined
Dancer uses plugins for shared functionality:
use Dancer2::Plugin::Database;
use Dancer2::Plugin::Auth::Tiny;
get '/api/protected' => require_login sub {
my $user = database->quick_select('users', { id => session->read('user_id') });
return to_json($user);
};
FastAPI's dependency injection is more explicit and testable:
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
async def get_current_user(token: str = Depends(oauth2_scheme)):
user = await verify_token(token)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication",
)
return user
@app.get("/api/protected")
async def protected_route(current_user: User = Depends(get_current_user)):
return current_user
The Depends() system is powerful, dependencies can have their own dependencies, creating clean, testable chains.
Async Native: From ForkManager to asyncio
Dancer processes requests synchronously by default. For concurrency, you scale processes:
# Deploy with Starman (pre-forking)
$ starman --workers 10 app.psgi
FastAPI is built for async/await from day one:
import asyncio
from fastapi import FastAPI
app = FastAPI()
@app.get("/api/slow-endpoint")
async def slow_endpoint():
# Non-blocking sleep, other requests process meanwhile
await asyncio.sleep(5)
return {"message": "Completed"}
For I/O-bound APIs (database calls, HTTP requests), this is transformative. One FastAPI process can handle hundreds of concurrent requests; you'd need dozens of Dancer workers for equivalent throughput.
Automatic Documentation: The Game Changer
Here's FastAPI's killer feature that Dancer simply can't match. Start your app:
$ uvicorn main:app --reload
Then visit:
- http://localhost:8000/docs, Interactive Swagger UI
- http://localhost:8000/redoc, Clean ReDoc documentation
Every route, parameter, request body and response model is documented automatically from your type hints. Your API documentation lives in your code and never goes stale.
For Dancer, documentation is a separate concern, Swagger plugins exist but require explicit annotation and maintenance.
Database Patterns: DBI vs Async ORMs
Dancer's database interaction is typically synchronous:
use Dancer2::Plugin::Database;
get '/api/users' => sub {
my @users = database->quick_select('users', {});
return to_json(\@users);
};
FastAPI pairs beautifully with async database libraries:
from fastapi import FastAPI
from databases import Database
app = FastAPI()
database = Database("postgresql://localhost/mydb")
@app.on_event("startup")
async def connect_db():
await database.connect()
@app.on_event("shutdown")
async def disconnect_db():
await database.disconnect()
@app.get("/api/users")
async def list_users():
query = "SELECT * FROM users"
rows = await database.fetch_all(query)
return rows
Or use SQLAlchemy 2.0's async support:
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.orm import sessionmaker
engine = create_async_engine("postgresql+asyncpg://localhost/mydb")
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
async def get_db():
async with async_session() as session:
yield session
@app.get("/api/users")
async def list_users(db: AsyncSession = Depends(get_db)):
result = await db.execute("SELECT * FROM users")
users = result.fetchall()
return users
Deployment: Plack vs ASGI
Dancer deploys via PSGI/Plack:
# Development
$ plackup app.psgi
# Production
$ starman --workers 10 --port 5000 app.psgi
$ hypnotoad myapp.pl # Mojolicious alternative
FastAPI deploys via ASGI servers:
# Development
$ uvicorn main:app --reload
# Production
$ uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4
$ gunicorn main:app -w 4 -k uvicorn.workers.UvicornWorker
Or containerize (both frameworks Dockerize well):
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
Testing: Test::More vs pytest
Dancer testing uses Plack::Test:
use Test::More;
use Plack::Test;
use HTTP::Request::Common;
my $test = Plack::Test->create(Dancer2->psgi_app);
my $res = $test->request(GET '/api/user/1');
is $res->code, 200;
like $res->content, qr/"name"/;
FastAPI's TestClient (via Starlette) is synchronous for testing:
from fastapi.testclient import TestClient
from main import app
client = TestClient(app)
def test_get_user():
response = client.get("/api/user/1")
assert response.status_code == 200
assert "name" in response.json()
def test_create_user():
response = client.post("/api/users", json={
"name": "Michael",
"email": "[email protected]"
})
assert response.status_code == 201
The TestClient handles the async event loop internally, you write synchronous tests for async code.
When to Choose What
Choose Dancer when: - You're maintaining legacy Perl infrastructure - Your team is primarily Perl-focused - You need CPAN's vast library ecosystem - You prefer explicit control over magic automation
Choose FastAPI when: - Starting a new API project in 2026 - Want automatic documentation without maintenance burden - Need high concurrency for I/O-bound workloads - Value type safety and IDE autocomplete - Deploying microservices or serverless functions
Migration Path from Dancer
If you're migrating a Dancer API to FastAPI:
- Start with route mapping, translate route patterns one-to-one
- Convert validation, move manual checks to Pydantic models
- Database layer, introduce async database drivers gradually
- Authentication, replace Dancer plugins with FastAPI dependencies
- Testing, rewrite tests using TestClient, keeping test cases
Example migration for a simple endpoint:
# Dancer version
get '/api/products' => sub {
my $category = query_parameters->get('category');
my $limit = query_parameters->get('limit') || 10;
my @products = database->quick_select('products', {
($category ? (category => $category) : ()),
}, { limit => $limit });
return to_json(\@products);
};
# FastAPI version
from typing import Optional, List
@app.get("/api/products", response_model=List[Product])
async def list_products(
category: Optional[str] = None,
limit: int = Query(10, ge=1, le=100)
):
query = "SELECT * FROM products WHERE 1=1"
values = {}
if category:
query += " AND category = :category"
values["category"] = category
query += " LIMIT :limit"
values["limit"] = limit
rows = await database.fetch_all(query, values)
return rows
The Bottom Line
Dancer taught me that web frameworks should get out of the way. FastAPI taught me that frameworks can be helpful without being intrusive. The automatic documentation, type-driven validation and async foundation make FastAPI feel like cheating, until you realize it's just Python's type system working as intended.
For Perl developers eyeing Python, FastAPI is the most familiar landing pad. It respects your need for clean routes, doesn't force MVC patterns when you don't want them and provides modern tooling without ceremonial boilerplate.
The lobster has left the building. 🦞🚀
Questions about API migrations? Drop them below.