Crear una API REST en Python con FastAPI: guía paso a paso
FastAPI es el framework web más rápido y moderno para construir APIs en Python. Genera documentación OpenAPI automáticamente, valida datos con Pydantic y ofrece soporte nativo para async/await.
1. Instalación y primera API
pip install fastapi uvicorn[standard]
# main.py
from fastapi import FastAPI
app = FastAPI(
title="Mi API",
description="API de ejemplo con FastAPI",
version="1.0.0",
)
@app.get("/")
def raiz():
return {"mensaje": "Hola, FastAPI!"}
@app.get("/salud")
def salud():
return {"estado": "ok"}
# Iniciar servidor con recarga automática
uvicorn main:app --reload
# Documentación interactiva automática
# http://localhost:8000/docs (Swagger UI)
# http://localhost:8000/redoc (ReDoc)
2. Parámetros de ruta y query
from fastapi import FastAPI, HTTPException, Query
from typing import Optional
app = FastAPI()
libros_db = {
1: {"titulo": "Clean Code", "autor": "Martin", "precio": 39.99},
2: {"titulo": "Python Tricks", "autor": "Bader", "precio": 29.99},
3: {"titulo": "Fluent Python", "autor": "Ramalho", "precio": 49.99},
}
# Parámetro de ruta
@app.get("/libros/{libro_id}")
def obtener_libro(libro_id: int):
if libro_id not in libros_db:
raise HTTPException(status_code=404, detail="Libro no encontrado")
return libros_db[libro_id]
# Parámetros de query con valores por defecto
@app.get("/libros")
def listar_libros(
skip: int = Query(default=0, ge=0),
limit: int = Query(default=10, ge=1, le=100),
autor: Optional[str] = None,
orden: str = Query(default="titulo", enum=["titulo", "precio"]),
):
libros = list(libros_db.values())
if autor:
libros = [l for l in libros if autor.lower() in l["autor"].lower()]
libros.sort(key=lambda l: l[orden])
return {"total": len(libros), "libros": libros[skip:skip+limit]}
3. Modelos Pydantic: validación automática
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field, validator
from typing import Optional
from datetime import datetime
app = FastAPI()
class LibroCrear(BaseModel):
titulo: str = Field(..., min_length=1, max_length=200)
autor: str = Field(..., min_length=1)
precio: float = Field(..., gt=0, le=9999.99)
isbn: Optional[str] = Field(None, pattern=r"^\d{13}$")
publicado: Optional[datetime] = None
@validator("titulo")
def titulo_no_vacio(cls, v):
return v.strip()
class LibroRespuesta(LibroCrear):
id: int
creado_en: datetime
class Config:
from_attributes = True
libros: dict[int, dict] = {}
siguiente_id = 1
@app.post("/libros", response_model=LibroRespuesta, status_code=201)
def crear_libro(libro: LibroCrear):
global siguiente_id
nuevo = {
"id": siguiente_id,
"titulo": libro.titulo,
"autor": libro.autor,
"precio": libro.precio,
"isbn": libro.isbn,
"publicado": libro.publicado,
"creado_en": datetime.now(),
}
libros[siguiente_id] = nuevo
siguiente_id += 1
return nuevo
@app.put("/libros/{libro_id}", response_model=LibroRespuesta)
def actualizar_libro(libro_id: int, datos: LibroCrear):
if libro_id not in libros:
raise HTTPException(404, "Libro no encontrado")
libros[libro_id].update(datos.dict(exclude_unset=True))
return libros[libro_id]
@app.delete("/libros/{libro_id}", status_code=204)
def eliminar_libro(libro_id: int):
if libro_id not in libros:
raise HTTPException(404, "Libro no encontrado")
del libros[libro_id]
4. Dependencias (Dependency Injection)
from fastapi import FastAPI, Depends, HTTPException, Header
from typing import Optional
app = FastAPI()
# Dependencia: paginación reutilizable
def paginacion(pagina: int = 1, por_pagina: int = 10):
if pagina < 1:
raise HTTPException(400, "La página debe ser >= 1")
return {"skip": (pagina - 1) * por_pagina, "limit": por_pagina}
@app.get("/productos")
def listar_productos(pag: dict = Depends(paginacion)):
# pag = {"skip": 0, "limit": 10}
return {"paginacion": pag, "productos": []}
# Dependencia: autenticación por token simple
API_KEYS = {"mi-api-key-secreta", "otra-key"}
def verificar_api_key(x_api_key: str = Header(...)):
if x_api_key not in API_KEYS:
raise HTTPException(status_code=401, detail="API key inválida")
return x_api_key
@app.get("/admin/stats", dependencies=[Depends(verificar_api_key)])
def estadisticas():
return {"usuarios": 1234, "conversiones_hoy": 567}
5. Endpoints async
import asyncio
import httpx
from fastapi import FastAPI
app = FastAPI()
@app.get("/clima/{ciudad}")
async def obtener_clima(ciudad: str):
async with httpx.AsyncClient() as client:
r = await client.get(
"https://api.open-meteo.com/v1/forecast",
params={"latitude": 40.4, "longitude": -3.7, "current_weather": True},
timeout=10,
)
r.raise_for_status()
datos = r.json()
return {"ciudad": ciudad, "clima": datos.get("current_weather")}
@app.get("/multiples")
async def multiples_peticiones():
async with httpx.AsyncClient() as client:
tareas = [
client.get("https://httpbin.org/anything/1"),
client.get("https://httpbin.org/anything/2"),
client.get("https://httpbin.org/anything/3"),
]
respuestas = await asyncio.gather(*tareas)
return [r.json() for r in respuestas]
6. CORS: permitir peticiones desde el navegador
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["https://mifrontend.com", "http://localhost:3000"],
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["*"],
)
@app.get("/datos")
def datos():
return {"datos": "accesibles desde el frontend"}
7. Subir archivos
from fastapi import FastAPI, UploadFile, File, HTTPException
from pathlib import Path
app = FastAPI()
EXTENSIONES_PERMITIDAS = {".jpg", ".png", ".pdf", ".docx"}
MAX_TAMANO = 10 * 1024 * 1024 # 10 MB
@app.post("/subir")
async def subir_archivo(archivo: UploadFile = File(...)):
ext = Path(archivo.filename).suffix.lower()
if ext not in EXTENSIONES_PERMITIDAS:
raise HTTPException(400, f"Extensión no permitida: {ext}")
contenido = await archivo.read()
if len(contenido) > MAX_TAMANO:
raise HTTPException(413, "Archivo demasiado grande (máximo 10 MB)")
destino = Path("uploads") / archivo.filename
destino.parent.mkdir(exist_ok=True)
destino.write_bytes(contenido)
return {
"nombre": archivo.filename,
"tamano": len(contenido),
"tipo": archivo.content_type,
}
8. Manejo global de errores
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
app = FastAPI()
@app.exception_handler(ValueError)
async def value_error_handler(request: Request, exc: ValueError):
return JSONResponse(
status_code=400,
content={"error": "Datos inválidos", "detalle": str(exc)},
)
@app.exception_handler(Exception)
async def error_generico(request: Request, exc: Exception):
return JSONResponse(
status_code=500,
content={"error": "Error interno del servidor"},
)
9. Routers: organizar endpoints en módulos
# routers/libros.py
from fastapi import APIRouter, HTTPException
router = APIRouter(prefix="/libros", tags=["Libros"])
@router.get("/")
def listar(): return []
@router.get("/{id}")
def obtener(id: int): ...
# routers/usuarios.py
from fastapi import APIRouter
router = APIRouter(prefix="/usuarios", tags=["Usuarios"])
@router.get("/")
def listar_usuarios(): return []
# main.py
from fastapi import FastAPI
from routers import libros, usuarios
app = FastAPI()
app.include_router(libros.router)
app.include_router(usuarios.router)
10. Despliegue básico
# Producción con Gunicorn + Uvicorn workers
pip install gunicorn
gunicorn main:app -w 4 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000
# Docker (Dockerfile básico)
# FROM python:3.12-slim
# WORKDIR /app
# COPY requirements.txt .
# RUN pip install -r requirements.txt
# COPY . .
# CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
Resumen de características clave
| Característica | FastAPI | Flask | Django REST |
|---|---|---|---|
| Async nativo | Sí | No | Parcial |
| Validación automática | Pydantic | Manual | Serializers |
| Docs OpenAPI | Auto | Extensión | Extensión |
| Velocidad | Alta | Media | Media |
| Curva aprendizaje | Baja | Muy baja | Media |
Conversiones relacionadas
Conversiones frecuentes del catálogo: