Three Frameworks, One Task: Fast API
When you need to build an API service — not a monolith with ORM and templates, but a pure JSON API with 10-50 endpoints and minimal latency — the stack choice determines everything: from performance to infrastructure costs.
Three frameworks compete for this segment: FastAPI (Python) with auto-documentation and types, Go Fiber with C-level performance, Node Fastify with the npm ecosystem and async/await. Each promises speed. Each solves the problem differently.
I've worked with FastAPI on an ML pipeline (inference API), Go Fiber on an auth microservice (50K+ rps), Fastify on a BFF layer (aggregating 5 microservices). Here's what I took away from each.
Architecture and Philosophy
FastAPI — Types and Docs Out of the Box
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, EmailStr
app = FastAPI(title="User API", version="1.0.0")
class UserCreate(BaseModel):
name: str
email: EmailStr
age: int | None = None
class UserResponse(BaseModel):
id: int
name: str
email: str
@app.post("/users", response_model=UserResponse, status_code=201)
async def create_user(user: UserCreate):
db_user = await db.users.insert(user.model_dump())
return UserResponse(**db_user)
Launch it — Swagger UI at /docs, ReDoc at /redoc. Pydantic types auto-generate JSON Schema.
Go Fiber — Minimalism and Speed
app := fiber.New(fiber.Config{Prefork: true})
app.Post("/users", func(c *fiber.Ctx) error {
var input UserCreate
if err := c.BodyParser(&input); err != nil {
return c.Status(400).JSON(fiber.Map{"error": "Invalid JSON"})
}
if err := validate.Struct(input); err != nil {
return c.Status(422).JSON(fiber.Map{"error": err.Error()})
}
user := db.CreateUser(input)
return c.Status(201).JSON(user)
})
app.Listen(":3000")
No auto-documentation (need swaggo). No auto-validation (need validator). But: goroutines instead of event loop, compiled binary, minimal overhead.
Node Fastify — Structured Express
const app = Fastify({ logger: true });
app.post('/users', {
schema: {
body: {
type: 'object',
required: ['name', 'email'],
properties: {
name: { type: 'string', minLength: 2 },
email: { type: 'string', format: 'email' },
},
},
},
}, async (request, reply) => {
const user = await db.users.create(request.body);
reply.code(201).send(user);
});
Fastify uses JSON Schema for validation (Ajv) and serialization (fast-json-stringify). Schema describes both input and output — Fastify generates an optimized serializer.
Architecture Comparison
| Aspect | FastAPI | Go Fiber | Node Fastify |
|---|---|---|---|
| Language | Python 3.10+ | Go 1.21+ | TypeScript/JS |
| Concurrency | async/await (asyncio) | Goroutines | Event loop |
| Validation | Pydantic (built-in) | go-validator | JSON Schema (Ajv) |
| Documentation | Swagger UI (built-in) | swaggo (third-party) | @fastify/swagger |
| Ecosystem | PyPI (500K+) | Go modules | npm (2M+) |
Benchmarks
Tests: 4 cores, 16 GB RAM, PostgreSQL 16 with 1M rows, wrk with 256 connections, 30 seconds.
Static JSON Response (no DB)
| Metric | FastAPI | Go Fiber | Node Fastify |
|---|---|---|---|
| Requests/sec | 18,500 | 185,000 | 62,000 |
| Latency p50 | 12ms | 1.2ms | 3.5ms |
| Memory | 85 MB | 12 MB | 65 MB |
CRUD with PostgreSQL
| Metric | FastAPI | Go Fiber | Node Fastify |
|---|---|---|---|
| Requests/sec | 4,200 | 12,800 | 8,500 |
| Latency p50 | 55ms | 18ms | 28ms |
| Memory | 120 MB | 25 MB | 95 MB |
With a database the gap shrinks: Go Fiber is 3x faster than FastAPI, Fastify 2x. The bottleneck moves to PostgreSQL.
Heavy Request (aggregation + external API)
| Metric | FastAPI | Go Fiber | Node Fastify |
|---|---|---|---|
| Requests/sec | 850 | 2,100 | 1,800 |
| Latency p50 | 280ms | 115ms | 135ms |
On I/O-bound tasks, Fastify approaches Go thanks to the event loop. FastAPI loses due to GIL and asyncio overhead.
Startup Time
| Metric | FastAPI | Go Fiber | Node Fastify |
|---|---|---|---|
| Cold start | 1.2s | 0.05s | 0.8s |
| Docker image | 150 MB | 12 MB | 80 MB |
Go has instant startup. Critical for serverless where cold start = money.
Middleware and Auth
FastAPI: Dependency Injection
async def get_current_user(
credentials: HTTPAuthorizationCredentials = Security(security),
) -> User:
payload = jwt.decode(credentials.credentials, SECRET_KEY)
user = await db.users.find_one(payload["user_id"])
if not user:
raise HTTPException(401, "Invalid token")
return user
@app.get("/admin/users")
async def admin_users(admin: User = Depends(require_admin)):
return await db.users.find_all()
Go Fiber: Middleware Chain
func AuthMiddleware() fiber.Handler {
return func(c *fiber.Ctx) error {
token := c.Get("Authorization")
claims, err := jwt.Parse(strings.TrimPrefix(token, "Bearer "))
if err != nil {
return c.Status(401).JSON(fiber.Map{"error": "Invalid token"})
}
c.Locals("user_id", claims.UserID)
return c.Next()
}
}
admin := app.Group("/admin", AuthMiddleware(), AdminOnly())
Node Fastify: Hooks
app.decorate('authenticate', async (request, reply) => {
const token = request.headers.authorization?.replace('Bearer ', '');
request.user = jwt.verify(token, process.env.JWT_SECRET!);
});
app.get('/admin/users', { preHandler: [app.authenticate] }, handler);
Testing
FastAPI
def test_create_product(client):
response = client.post("/products", json={"name": "Test", "price": 29.99, "category": "test"})
assert response.status_code == 201
assert response.json()["name"] == "Test"
Go Fiber
req := httptest.NewRequest("POST", "/products", strings.NewReader(body))
resp, _ := app.Test(req, -1)
assert.Equal(t, 201, resp.StatusCode)
Node Fastify
const response = await app.inject({
method: 'POST', url: '/products',
payload: { name: 'Test', price: 29.99, category: 'test' },
});
assert.strictEqual(response.statusCode, 201);
When to Choose What
FastAPI
- ML/AI inference API: PyTorch, TensorFlow, scikit-learn integration
- Data-heavy API: pandas, numpy in the pipeline
- Rapid prototyping: auto-documentation saves days
- Team knows Python best
- Load < 5K rps
Go Fiber
- High-performance API: > 10K rps, low latency critical
- Microservices: tiny Docker image, instant cold start
- Serverless (Lambda): 50ms cold start vs 1200ms Python
- Infrastructure services: proxy, gateway, auth
- Predictable performance without GC pauses
Node Fastify
- BFF (Backend-for-Frontend): aggregating multiple APIs
- Real-time: WebSocket + HTTP in one process
- Fullstack TypeScript: shared types between front and back
- npm ecosystem is critical
- Frontend team writing backend
- Load 5-15K rps
None of the Three
- Monolith with ORM, migrations, templates → Rails, Laravel, Django
- Enterprise CQRS, DDD → Spring Boot, .NET
- Systems programming → Rust (Actix, Axum)
Final Checklist
Choice
- [ ] Primary load: CPU-bound → Go, I/O-bound → Fastify/FastAPI, ML → FastAPI
- [ ] Target rps: < 5K → any, 5-15K → Fastify/Go, > 15K → Go
- [ ] Ecosystem: PyPI (data/ML), npm (frontend), Go modules (infra)
- [ ] Cold start: serverless → Go, containers → any
Architecture
- [ ] Framework-level validation: Pydantic / JSON Schema / go-validator
- [ ] Structured errors: uniform
{"error": ..., "code": ...} - [ ] Health check:
GET /healthwith dependency checks - [ ] Graceful shutdown: handle SIGTERM
Production
- [ ] Rate limiting: built-in or nginx
- [ ] CORS: configure for specific domains
- [ ] Structured logging: JSON logs for collector
- [ ] Metrics: Prometheus
/metrics - [ ] Docker: multi-stage build, non-root user
Comments (0)