Consumir una API externa parece inofensivo al principio. Haces un requests.get, recibes un diccionario, y lo usas tal cual en el resto del código. El problema empieza cuando esa misma estructura JSON termina diseminada en diez archivos, y la API renombra un campo o cambia price de float a string. Corregirlo se convierte en una búsqueda del tesoro.

La capa anti-corrupción (Anti-Corruption Layer, o ACL) responde a este problema. Procedente del Domain-Driven Design, actúa como un traductor entre un sistema externo y tu lógica de negocio. Un solo punto de contacto, un solo lugar que modificar cuando la API cambia.

El problema: un acoplamiento difuso

Imaginemos un servicio que recupera información de coches desde una API de terceros:

def display_car(url: str) -> None:
    response = requests.get(url)
    data = response.json()
    print(f"{data['brand']} {data['model']} - {data['color_hex']}")

def price_with_tax(url: str) -> float:
    response = requests.get(url)
    data = response.json()
    return float(data['price']) * 1.20

El diccionario devuelto por la API circula por todas partes. Si el proveedor renombra color_hex a paint_code, hay que modificar cada función que lo usa. Peor aún, si price pasa de "15000.00" a 15000, las conversiones float() esparcidas por el código se convierten en bombas de tiempo.

Este acoplamiento es invisible hasta el día en que duele. Es una forma particularmente insidiosa de connascencia: la estructura de un dato externo se convierte en un contrato implícito compartido por todo el código.

La solución: un objeto de dominio dedicado

La ACL introduce un tipo intermedio que representa lo que tu aplicación entiende, no lo que la API devuelve:

from dataclasses import dataclass
import requests

@dataclass(frozen=True)
class Car:
    brand: str
    model: str
    color: str
    price: float

def fetch_car(url: str) -> Car:
    response = requests.get(url, timeout=5)
    response.raise_for_status()
    data = response.json()
    return Car(
        brand=data["brand"],
        model=data["model"],
        color=data["color_hex"],
        price=float(data["price"]),
    )

El resto de la aplicación solo manipula instancias de Car. Las transformaciones (renombrar color_hex a color, convertir price a float) se concentran en fetch_car. El frozen=True impide cualquier mutación accidental de un objeto destinado a reflejar un dato externo. Para más sobre las sutilezas de las dataclasses inmutables, ver field(default_factory) en profundidad.

Lo que la ACL aporta realmente

El beneficio evidente es la resistencia a los cambios externos. Si la API renombra un campo, tocas una sola función. Pero la ACL aporta otras tres cosas menos visibles.

Un vocabulario de dominio limpio. Una API puede devolver usr_first_nm, dob_iso, acct_st. Tu aplicación no debería convivir con esos nombres. La ACL los traduce en first_name, birth_date, account_status, lo que hace el código llamador legible sin contexto externo.

Una frontera clara entre I/O y lógica. Toda la parte de red (timeout, errores HTTP, parsing JSON) permanece en la capa. La lógica de negocio no sabe nada de requests ni de httpx. Esto facilita las pruebas: se reemplaza fetch_car por un stub que devuelve un Car, sin montar un mock HTTP.

Tipado de extremo a extremo. El diccionario data no tiene tipo alguno. Una vez convertido en Car, mypy o Pyright pueden verificar cada acceso. Los errores de campo se vuelven errores de lint, no errores de ejecución.

Gestionar los campos ausentes o inválidos

El código de la nota original usaba data.get("brand"), que devuelve None si la clave falta. Es una falsa seguridad: None se propaga por el código y explota más adelante, lejos de la causa real.

La ACL es el lugar ideal para validar:

@dataclass(frozen=True)
class Car:
    brand: str
    model: str
    color: str
    price: float

def fetch_car(url: str) -> Car:
    response = requests.get(url, timeout=5)
    response.raise_for_status()
    data = response.json()
    try:
        return Car(
            brand=data["brand"],
            model=data["model"],
            color=data["color_hex"],
            price=float(data["price"]),
        )
    except (KeyError, ValueError, TypeError) as exc:
        raise ValueError(f"Respuesta API inválida: {exc}") from exc

El error se lanza en el momento en que es detectable, con un mensaje explícito. El resto del código recibe o un Car válido, o una excepción clara.

Para contratos más complejos, Pydantic cumple el mismo papel con validación declarativa:

from pydantic import BaseModel, Field

class Car(BaseModel):
    brand: str
    model: str
    color: str = Field(alias="color_hex")
    price: float

def fetch_car(url: str) -> Car:
    response = requests.get(url, timeout=5)
    response.raise_for_status()
    return Car.model_validate(response.json())

Pydantic gestiona el renombrado mediante alias, las conversiones de tipo, y lanza un ValidationError detallado si la respuesta no coincide con el esquema esperado.

Cuándo no poner una ACL

La ACL tiene un coste: un tipo adicional que mantener, una función de conversión que escribir. Para un script que llama a una API una sola vez y muestra el resultado, es sobre-ingeniería.

El patrón se vuelve pertinente en cuanto:

  • el mismo dato externo se consume en varios lugares
  • el vocabulario de la API difiere del de tu dominio
  • la API es inestable o está fuera de tu control
  • quieres poder reemplazar el proveedor sin reescribir el código de negocio

A la inversa, para una llamada puntual a tu propia API interna cuyo contrato controlas, la ACL aporta poco.

Conclusión

La capa anti-corrupción no es un patrón complejo. Es una disciplina: rechazar dejar que las estructuras externas se filtren en el código de negocio. El dict devuelto por una API permanece en la frontera, y todo lo que circula dentro de la aplicación es un objeto tipado, validado y nombrado según tu propio vocabulario.

El día que la API cambie, sabrás exactamente qué archivo abrir.