< Back

🚀 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:

```perl 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:

```python 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:

```perl 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:

```python 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:

```perl 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:

```python 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:

```perl

Deploy with Starman (pre-forking)

$ starman --workers 10 app.psgi ```

FastAPI is built for async/await from day one:

```python 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:

bash $ 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:

```perl 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:

```python 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:

```python 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:

```bash

Development

$ plackup app.psgi

Production

$ starman --workers 10 --port 5000 app.psgi $ hypnotoad myapp.pl # Mojolicious alternative ```

FastAPI deploys via ASGI servers:

```bash

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):

dockerfile 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:

```perl 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:

```python 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:

  1. Start with route mapping, translate route patterns one-to-one
  2. Convert validation, move manual checks to Pydantic models
  3. Database layer, introduce async database drivers gradually
  4. Authentication, replace Dancer plugins with FastAPI dependencies
  5. Testing, rewrite tests using TestClient, keeping test cases

Example migration for a simple endpoint:

```perl

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);

}; ```

```python

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.


< Back