En esta página
Introducción a FastAPI
¿Qué es FastAPI?
FastAPI es un framework web moderno para construir APIs en Python. Fue creado por Sebastián Ramírez y publicado en 2018. Rápidamente se convirtió en uno de los frameworks más populares de Python gracias a tres características excepcionales:
- Velocidad: Comparable con Node.js y Go gracias a Starlette y Uvicorn (ASGI)
- Validación automática: Usa Pydantic para validar datos de entrada y salida
- Documentación automática: Genera Swagger UI y ReDoc sin escribir una línea extra
FastAPI es el framework elegido por empresas como Microsoft, Uber, Netflix y decenas de startups para sus servicios internos.
Instalación
# FastAPI y Uvicorn (servidor ASGI)
pip install fastapi uvicorn[standard]
# Pydantic ya viene incluido con FastAPI
# Para desarrollo también instala:
pip install python-multipart httpx # Para formularios y testsTu primera aplicación
# archivo: main.py
from fastapi import FastAPI
app = FastAPI(
title="Mi Primera API",
description="Una API de ejemplo con FastAPI",
version="0.1.0"
)
@app.get("/")
async def raíz() -> dict:
return {"mensaje": "¡Hola desde FastAPI!", "version": "0.1.0"}
@app.get("/salud")
async def verificar_salud() -> dict:
return {"estado": "saludable", "servicio": "Mi API"}Para ejecutar:
uvicorn main:app --reload
# INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
# INFO: Started reloader process using StatReloadAhora visita:
http://localhost:8000/— Tu endpoint raízhttp://localhost:8000/docs— Swagger UI interactivohttp://localhost:8000/redoc— Documentación ReDoc
Parámetros de ruta
Los parámetros de ruta se definen con llaves {} en el decorador:
from fastapi import FastAPI, Path
app = FastAPI()
productos = {
1: {"nombre": "Laptop", "precio": 1299.99},
2: {"nombre": "Mouse", "precio": 29.99},
3: {"nombre": "Teclado", "precio": 89.99},
}
@app.get("/productos/{producto_id}")
async def obtener_producto(
producto_id: int = Path(..., ge=1, description="ID del producto")
) -> dict:
if producto_id not in productos:
from fastapi import HTTPException
raise HTTPException(status_code=404, detail=f"Producto {producto_id} no encontrado")
return productos[producto_id]
@app.get("/usuarios/{usuario_id}/posts/{post_id}")
async def obtener_post_de_usuario(usuario_id: int, post_id: int) -> dict:
return {"usuario_id": usuario_id, "post_id": post_id}Parámetros de consulta (query params)
Los parámetros de consulta son los que van después del ? en la URL:
from fastapi import FastAPI, Query
@app.get("/productos")
async def listar_productos(
pagina: int = Query(1, ge=1, description="Número de página"),
limite: int = Query(10, ge=1, le=100, description="Elementos por página"),
buscar: str | None = Query(None, min_length=2, description="Texto de búsqueda"),
ordenar_por: str = Query("nombre", regex="^(nombre|precio)$"),
) -> dict:
todos = list(productos.values())
if buscar:
todos = [p for p in todos if buscar.lower() in p["nombre"].lower()]
inicio = (pagina - 1) * limite
fin = inicio + limite
return {
"total": len(todos),
"pagina": pagina,
"limite": limite,
"datos": todos[inicio:fin]
}Modelos Pydantic para el body de la petición
Pydantic es la columna vertebral de la validación en FastAPI:
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field, EmailStr, validator
from typing import Annotated
from datetime import datetime
# Modelo para crear (entrada)
class UsuarioCrear(BaseModel):
nombre: str = Field(..., min_length=2, max_length=100, description="Nombre completo")
email: EmailStr = Field(..., description="Email válido")
edad: int = Field(..., ge=13, le=120, description="Edad en años")
sitio_web: str | None = Field(None, description="URL opcional del sitio web")
# Validador personalizado (Pydantic v2)
@validator("nombre")
def nombre_no_puede_ser_solo_espacios(cls, v: str) -> str:
if v.strip() == "":
raise ValueError("El nombre no puede ser solo espacios")
return v.strip()
# Modelo de respuesta (salida) — puede tener más campos
class Usuario(UsuarioCrear):
id: int
activo: bool = True
creado_en: datetime = Field(default_factory=datetime.now)
class Config:
# En Pydantic v2 esto cambia a model_config = ConfigDict(...)
json_schema_extra = {
"example": {
"nombre": "Ana García",
"email": "[email protected]",
"edad": 28,
"sitio_web": "https://ana.dev"
}
}
# Base de datos simulada
usuarios_db: dict[int, Usuario] = {}
id_counter = 0
@app.post("/usuarios", response_model=Usuario, status_code=201)
async def crear_usuario(datos: UsuarioCrear) -> Usuario:
global id_counter
# Verificar email único
for u in usuarios_db.values():
if u.email == datos.email:
raise HTTPException(
status_code=409,
detail=f"Ya existe un usuario con el email {datos.email}"
)
id_counter += 1
usuario = Usuario(id=id_counter, **datos.model_dump())
usuarios_db[id_counter] = usuario
return usuario
@app.get("/usuarios", response_model=list[Usuario])
async def listar_usuarios(activo: bool | None = None) -> list[Usuario]:
usuarios = list(usuarios_db.values())
if activo is not None:
usuarios = [u for u in usuarios if u.activo == activo]
return usuarios
@app.get("/usuarios/{usuario_id}", response_model=Usuario)
async def obtener_usuario(usuario_id: int) -> Usuario:
if usuario_id not in usuarios_db:
raise HTTPException(status_code=404, detail="Usuario no encontrado")
return usuarios_db[usuario_id]
@app.put("/usuarios/{usuario_id}", response_model=Usuario)
async def actualizar_usuario(usuario_id: int, datos: UsuarioCrear) -> Usuario:
if usuario_id not in usuarios_db:
raise HTTPException(status_code=404, detail="Usuario no encontrado")
usuario_actualizado = Usuario(id=usuario_id, **datos.model_dump())
usuarios_db[usuario_id] = usuario_actualizado
return usuario_actualizado
@app.delete("/usuarios/{usuario_id}", status_code=204)
async def eliminar_usuario(usuario_id: int) -> None:
if usuario_id not in usuarios_db:
raise HTTPException(status_code=404, detail="Usuario no encontrado")
del usuarios_db[usuario_id]Manejo de errores con HTTPException
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import JSONResponse
# Manejador global de excepciones personalizadas
class ErrorDeNegocio(Exception):
def __init__(self, mensaje: str, codigo: int = 400) -> None:
self.mensaje = mensaje
self.codigo = codigo
@app.exception_handler(ErrorDeNegocio)
async def manejar_error_negocio(request: Request, exc: ErrorDeNegocio) -> JSONResponse:
return JSONResponse(
status_code=exc.codigo,
content={"error": exc.mensaje, "tipo": "error_negocio"}
)
@app.get("/operacion-arriesgada")
async def operacion_arriesgada(valor: int) -> dict:
if valor < 0:
raise ErrorDeNegocio("El valor no puede ser negativo", codigo=400)
if valor > 1000:
raise HTTPException(
status_code=422,
detail={
"mensaje": "Valor demasiado grande",
"maximo": 1000,
"recibido": valor
}
)
return {"resultado": valor * 2}Middleware y dependencias
from fastapi import FastAPI, Depends, Header
from typing import Annotated
# Dependencia para autenticación
async def verificar_token(
authorization: Annotated[str | None, Header()] = None
) -> str:
if not authorization or not authorization.startswith("Bearer "):
raise HTTPException(
status_code=401,
detail="Token de autorización requerido",
headers={"WWW-Authenticate": "Bearer"}
)
token = authorization.removeprefix("Bearer ")
if token != "mi-token-secreto":
raise HTTPException(status_code=403, detail="Token inválido")
return token
@app.get("/ruta-protegida")
async def ruta_protegida(token: Annotated[str, Depends(verificar_token)]) -> dict:
return {"mensaje": "Acceso concedido", "token": token[:10] + "..."}Eventos de inicio y cierre
from contextlib import asynccontextmanager
from typing import AsyncGenerator
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator:
# Código que se ejecuta al INICIO
print("Iniciando la aplicación...")
# Aquí conectarías a la base de datos, cargarías modelos ML, etc.
yield # La aplicación corre aquí
# Código que se ejecuta al CIERRE
print("Cerrando la aplicación...")
# Aquí cerrarías conexiones, guardarías estado, etc.
app = FastAPI(lifespan=lifespan)Estructura de proyecto recomendada
Para proyectos reales, organiza tu FastAPI así:
mi_api/
├── main.py # Punto de entrada
├── routers/
│ ├── __init__.py
│ ├── usuarios.py # APIRouter de usuarios
│ └── productos.py # APIRouter de productos
├── models/
│ ├── __init__.py
│ └── usuario.py # Modelos Pydantic
├── services/
│ └── usuario_service.py # Lógica de negocio
└── dependencies.py # Dependencias compartidas# routers/usuarios.py
from fastapi import APIRouter
router = APIRouter(prefix="/usuarios", tags=["Usuarios"])
@router.get("/")
async def listar() -> list: ...
@router.get("/{uid}")
async def obtener(uid: int) -> dict: ...
# main.py
from fastapi import FastAPI
from routers import usuarios, productos
app = FastAPI()
app.include_router(usuarios.router)
app.include_router(productos.router)Resumen
FastAPI es uno de los frameworks más modernos y eficientes para construir APIs en Python. Combina la velocidad de Starlette/ASGI con la validación automática de Pydantic y documentación Swagger sin esfuerzo adicional. En la próxima lección profundizaremos en los decoradores y el sistema de type hints de Python.
Inicia sesión para guardar tu progreso