Skip to content

FastAPI vs NestJS: When Simplicity Becomes a Trap

I started a new project last month. Picked FastAPI because everyone said it was “simple” and “minimal boilerplate.”

Six weeks later, my main.py had 47 endpoints, database queries scattered everywhere, and the new hire asked: “Where does business logic live?”

My answer: “Everywhere and nowhere.”

That’s when I realized: simplicity isn’t always simple.

The Problem: Simplicity Has a Hidden Cost

FastAPI is undeniably simpler on day one:

main.py
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class User(BaseModel):
name: str
email: str
@app.post("/users")
async def create_user(user: User):
return {"id": 1, "name": user.name, "email": user.email}
# Total: 10 lines. Done.

Compare to NestJS:

users.controller.ts
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Post()
create(@Body() createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
}
}
users.service.ts
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private userRepository: Repository<User>,
) {}
async create(createUserDto: CreateUserDto): Promise<User> {
const user = this.userRepository.create(createUserDto);
return this.userRepository.save(user);
}
}
users.module.ts
@Module({
imports: [TypeOrmModule.forFeature([User])],
controllers: [UsersController],
providers: [UsersService],
})
export class UsersModule {}

NestJS requires ~50 lines across 5 files for the same feature.

But here’s what I missed: FastAPI’s simplicity isn’t free. It’s borrowed against your future self.

When Simplicity Backfires

Month 1: Everything is Great

I could add endpoints in minutes. No service layers, no modules, no ceremony:

main.py (week 1)
@app.get("/users/{user_id}")
async def get_user(user_id: int):
user = await db.query("SELECT * FROM users WHERE id = $1", user_id)
return user
@app.post("/orders")
async def create_order(order: OrderCreate):
if order.total < 0:
raise HTTPException(400, "Invalid total")
await db.query("INSERT INTO orders...")
await email_service.send(...)
return {"status": "created"}

This felt productive. I was shipping features fast.

Month 2: The cracks appear

Then the team grew. New developers joined. Questions started:

  • “Where do I add validation?”
  • “Should this logic go in the route or a separate file?”
  • “How do I test this endpoint in isolation?”

The answer to all of them: “It depends.” Because FastAPI doesn’t tell you.

Meanwhile, a parallel project using NestJS had zero such questions. The framework enforces where things go:

NestJS structure (enforced)
src/
├── users/
│ ├── users.controller.ts # Routes go here
│ ├── users.service.ts # Business logic goes here
│ ├── users.module.ts # Dependencies go here
│ └── dto/
│ └── create-user.dto.ts # Validation goes here

New NestJS developer: “Where do I add a new endpoint?”

Answer: “Controller. Always. The framework literally won’t let you put it elsewhere.”

The Trade-off Spectrum

I diagrammed this for my team:

Framework trade-offs
SIMPLICITY STRUCTURE
| |
FastAPI ------------------------------------> NestJS
| |
| |
+------------------+ +------------------+
| Less boilerplate | | More boilerplate |
| Your choices | | Framework choices |
| Flexibility | | Consistency |
| Fast start | | Slower start |
| Chaotic scale | | Orderly scale |
+------------------+ +------------------+

The Reddit thread that sparked this comparison had a telling comment:

“FastAPI is the simplest/least bloated… Doesn’t mean it’s the best, but it does mean you can get more done with less code if you know what you’re doing.”

That “if you know what you’re doing” is doing a lot of heavy lifting.

The Real Cost: Decision Fatigue

FastAPI gives you freedom. But freedom requires decisions.

Every feature, I had to decide:

  • Should this be a separate service or inline?
  • Where does validation go?
  • How do I structure my files?
  • Should I use dependency injection?
  • What about database sessions?

NestJS answers all of these upfront:

  • Service: Yes, always separate
  • Validation: DTOs with decorators
  • File structure: One module per feature
  • DI: Built-in, use constructor injection
  • Database: Inject repository

The “boilerplate” I complained about? It’s actually pre-made decisions.

When I’d Actually Choose FastAPI

Despite the chaos, FastAPI is still my choice for certain projects:

Choose FastAPI when:

  1. You know architectural patterns cold - You don’t need guardrails
  2. Prototyping - 2-week project that won’t scale
  3. Solo/small team with shared vision - No onboarding needed
  4. ML/AI integration - Python ecosystem wins
  5. Performance is critical - FastAPI’s async-first design
fastapi_ml_api.py
# When FastAPI shines: ML inference API
from fastapi import FastAPI
from pydantic import BaseModel
import torch
app = FastAPI()
model = torch.load("model.pt") # Load once at startup
class PredictRequest(BaseModel):
features: list[float]
@app.post("/predict")
async def predict(req: PredictRequest):
with torch.no_grad():
tensor = torch.tensor(req.features)
return {"prediction": model(tensor).item()}
# 20 lines, one file, done. Perfect for this use case.

Choose NestJS when:

  1. Team has mixed experience - Junior devs need structure
  2. Project will grow - 6+ months, multiple developers
  3. Enterprise environment - Code reviews, compliance
  4. Microservices - Module system shines here
  5. Long-term maintenance matters - Onboarding new devs regularly
orders.service.ts
// When NestJS shines: Complex business logic
@Injectable()
export class OrdersService {
constructor(
private readonly ordersRepo: OrdersRepository,
private readonly inventoryService: InventoryService,
private readonly paymentService: PaymentService,
private readonly emailService: EmailService,
private readonly auditLog: AuditLogService,
) {}
async create(dto: CreateOrderDto): Promise&lt;Order&gt; {
// Clear separation: validation is in DTO
// Clear separation: orchestration is in service
// Clear separation: data access is in repository
// New dev knows exactly where to find things
await this.inventoryService.reserve(dto.items);
const order = await this.ordersRepo.create(dto);
await this.paymentService.charge(order);
await this.emailService.sendConfirmation(order);
await this.auditLog.log('order_created', order);
return order;
}
}

What I Wish I’d Known

The mistake wasn’t choosing FastAPI. The mistake was choosing FastAPI without a plan.

FastAPI requires discipline to scale. You need to self-impose the structure NestJS gives you for free:

project_structure.py
# FastAPI CAN be structured - but YOU must do it
project/
├── app/
│ ├── api/
│ │ └── users.py # Routes only
│ ├── services/
│ │ └── user_service.py # Business logic
│ ├── models/
│ │ └── user.py # Database models
│ ├── schemas/
│ │ └── user.py # Pydantic schemas
│ └── core/
│ └── config.py # Configuration

The difference? NestJS enforces this. FastAPI allows this.

My New Rule

Before choosing a framework, I ask:

  1. What’s the team size in 6 months?
  2. Will new developers join?
  3. Is this a prototype or production?
  4. Do I have strong architectural opinions?

If the answers are “big, yes, production, yes” -> FastAPI with strict structure

If “big, yes, production, no” -> NestJS

If “small, no, prototype, doesn’t matter” -> FastAPI, minimal structure

The Verdict

FastAPI is simpler. But simplicity is a feature, not a virtue.

FastAPI gives you:

  • Less code to write initially
  • No framework-enforced patterns
  • Freedom to architect as you see fit
  • “Less hidden magic” - you know exactly what your code does

NestJS gives you:

  • Predictable project structure
  • Enforced best practices
  • Faster team onboarding
  • Less architectural decision fatigue

The real question isn’t “Which is simpler?” It’s “What do I need: freedom or guardrails?”

  • FastAPI = Freedom with responsibility
  • NestJS = Guardrails with overhead

Both are excellent. Neither is universally better.

My 47-endpoint main.py? I refactored it into a proper structure. Took two weeks. The NestJS project? It would have been structured from day one.

Choose accordingly.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments