Diseñar la API de una biblioteca desde el dominio
Metadatos de la lección
- Autoría:
- Ignacio Slater-Muñoz
- Última actualización:
- 7 de abril de 2026
Abstract
En la lección anterior establecimos que una biblioteca es una API: un contrato entre quienes la implementan y quienes la usan. Pero ¿con qué criterios discutimos si ese contrato es de buena calidad? Esta lección propone un primer vocabulario: cinco principios que ayudan a construir APIs claras, robustas y orientadas al uso real.
Una buena API refleja los conceptos del problema, protege reglas críticas, limita deliberadamente su superficie, comunica claramente y anticipa cómo quien consume la usará. El objetivo no es agotar cada principio, sino dotarte de criterios para nombrar decisiones de diseño y reconocer estos temas cuando reaparezcan en contextos más concretos del curso.
Modelo del dominio: la API refleja conceptos del problema
Primer principio
Si el problema es hacer visible y accesible el mundo que la biblioteca modela, entonces el nombre de cada tipo y función debe nombrar un concepto real del dominio, no un detalle de implementación.
Una API bien diseñada debe modelar con claridad los conceptos centrales del problema que resuelve. Esto significa que sus tipos, funciones y estructuras deben nombrar conceptos reales del mundo que modelan, no abstracciones de implementación ni detalles técnicos.
Modelo de dominio en una API
Un modelo de dominio es la representación de los conceptos, reglas y relaciones que una biblioteca necesita expresar para resolver un problema.
La API debería exponer como tipos explícitos aquellos conceptos del dominio
que tienen identidad, reglas o restricciones propias. Para que eso funcione, los
nombres deben ser suficientemente claros dentro del contexto de la biblioteca: un
tipo como Address comunica mejor que uno ambiguo como Direction si lo que se modela es una dirección postal. Cuando el contexto no
basta, conviene preferir nombres más específicos que ayuden a entender la intención
sin depender por completo de documentación externa.
- Nombres claros: cada tipo o función debe nombrar un concepto del dominio, no una estructura de datos. Prefiere
AccountsobreHashMap. - Relaciones explícitas: si el dominio dice que una orden de compra tiene múltiples artículos, la API debe reflejar esa relación de forma clara y segura.
- Invariantes representados: si una transferencia bancaria nunca puede tener monto negativo, usa un tipo que imposibilite esa situación, no validaciones posteriores.
Importante
transfer()transfer(), validate()validate(),
issueInvoice()issueInvoice() también son parte del modelo. Cada una expresa
intención del dominio, no solo cómo cambian los datos internos.
Caso de estudio: biblioteca de direcciones (modelado de dominio)
Mal diseño
Exponer solo una función comovalidate_address(str)validate_address(str)
que reciba toda la dirección como texto. Así, quien consume debe parsear,
separar componentes y manejar por su cuenta gran parte de los errores del
dominio. Buen diseño
Exponer tipos comoCountryCountry, PostalCodePostalCode y AddressAddress, con validaciones encapsuladas. Así,
quien consume construye una AddressAddress usando
componentes con significado propio, y la biblioteca puede proteger
invariantes como la relación entre país y código postal. Esto no impide ofrecer una función de conveniencia para parsear direcciones desde texto, pero esa función no debería reemplazar a una API cuyo modelo principal refleje los conceptos del dominio.
Pitfall: usar primitivos en lugar de tipos de dominio
En una biblioteca de direcciones, modelar todo como vuelve más fácil confundir campos, aceptar combinaciones inválidas o
postergar reglas importantes hasta muy tarde. Tipos como StringString, CountryCountry o PostalCodePostalCode
permiten expresar mejor el dominio y hacer que ciertos errores sean más
difíciles de cometer.
AddressAddress
Criterio: cuándo introducir un tipo de dominio
No todo concepto del dominio merece su propio tipo público. Introduce tipos cuando el concepto cumple al menos una de estas características:
- Tiene reglas o restricciones: p. ej., un código postal válido, un monto no negativo, un correo con formato específico.
- Tiene identidad o significado propio: p. ej.,
no es solo un número; es un concepto con historial, propietario, estado.AccountAccount - Necesita métodos especializados: p. ej.,
podría tenerAddressAddressonormalize()normalize().distance_to(other)distance_to(other)
Si un concepto es solo un agregado transitorio de datos sin reglas, un tipo primitivo o una tupla/record anónima puede bastar.
Encapsulacion: ocultar implementacion y proteger invariantes
Segundo principio
Si el problema es garantizar que ciertos invariantes nunca puedan romperse y que la biblioteca pueda evolucionar sin sorpresas, entonces la implementación debe quedarse privada y las reglas críticas deben ser imposibles de romper desde afuera.
La encapsulación es el acto de ocultar detalles de implementación tras una interfaz pública clara. Permite que tu biblioteca evolucione sin romper el código de quien la usa, y protege los invariantes del dominio (las reglas que nunca pueden romperse).
Encapsulacion en una API
Una API encapsulada expone solo la información que quien consume necesita conocer y mantiene privados los detalles internos: cómo se almacenan los datos, qué algoritmos se usan, qué estructuras de caché o índices hay. Si el dominio tiene reglas invariantes (p. ej., una orden siempre debe tener al menos un articulo), la encapsulación garantiza que esas reglas no puedan romperse desde afuera.
Tip
- Ocultar detalles: no expongas estructuras internas como si fueran parte de la interfaz pública. Usa visibilidades privadas y módulos para separar lo interno de lo público.
- Proteger invariantes: si la regla del dominio dice "un saldo nunca puede ser negativo", haz imposible desde fuera romper esa regla. Usa constructores privados y métodos verificadores.
- Libertad de evolucionar: si quieres cambiar la estructura interna de una clase sin afectar consumidores, primero debes haber ocultado esa estructura tras métodos públicos.
Ejemplo: cuenta bancaria con invariantes (encapsulación)
Una cuenta bancaria tiene saldo. La regla invariante es: «el saldo nunca
puede ser negativo» . Si expones el saldo como campo público
modificable, alguien podría hacer ,
rompiendo el invariante.
account.balance = -100account.balance = -100
Solución
Minimalidad útil: pequeño pero completo
Tercer principio
Si el problema es que quien usa acceda fácilmente a lo que necesita sin abrumarle con opciones innecesarias, entonces cada elemento público debe justificar claramente su existencia.
Una API debería ser tan pequeña como sea posible sin dejar vacíos importantes en el problema que pretende resolver. Eso implica exponer solo los tipos, funciones y métodos que aportan valor real, porque el exceso de opciones aumenta la complejidad, confunde a quien consume y amplía la superficie de error.
¿Qué significa «minimalidad útil» ?
Minimalidad útil no significa ofrecer menos de lo necesario, sino evitar variantes, extensiones y conveniencias que no aportan claramente al problema que la biblioteca resuelve. Una API pequeña se aprende más rápido, se usa con menos ambigüedad y también suele ser más fácil de mantener y evolucionar.
- No expongas alternativas equivalentes: si una operación ya está bien representada por
, evita sumar variantes comoget_balance()get_balance(),fetch_balance()fetch_balance()oconsult_balance()consult_balance()sin una diferencia real de comportamiento.balance_getter()balance_getter() - No incluyas funcionalidades «por si acaso» : no agregues opciones que nadie necesita todavía. Cada elemento público nuevo amplía la API y hace más difícil su evolución futura.
- Evita operaciones demasiado específicas: si un caso particular puede resolverse combinando bien la API principal, probablemente no necesita una función pública propia.
- Pero sí, sé completo: si una capacidad forma parte del problema central que la biblioteca promete resolver, no la omitas. La minimalidad útil elimina lo accesorio, no lo esencial.
Núcleo y conveniencias: un equilibrio necesario
Una API puede tener un núcleo pequeño y compacto, y además ofrecer funciones de conveniencia para hacer más cómodo su uso. La clave es que cada conveniencia justifique claramente su existencia: debe resolver un caso frecuente, ahorrar código repetitivo o mejorar la claridad sin introducir ambigüedad.
Idealmente, las funciones de conveniencia deberían construirse sobre el núcleo, no reemplazarlo ni duplicarlo con reglas distintas. Si una conveniencia no puede explicarse en términos del núcleo, quizás no sea solo una ayuda ergonómica, sino una capacidad distinta que requiere su propio diseño.
Por ejemplo, una biblioteca de direcciones podría exponer un núcleo como
y además
ofrecer una conveniencia como create_from_parts(country, postal_code, street)create_from_parts(country, postal_code, street)
para casos en que la entrada inicial llega como texto. La segunda no reemplaza al
modelo principal, pero puede ser útil si simplifica un caso de uso frecuente.
parse_address(raw_address)parse_address(raw_address)
Caso de estudio: interfaz de carrito de compras (minimalidad útil)
Una biblioteca de carrito de compras podría exponer innecesariamente métodos como
, get_item_by_index()get_item_by_index(), sort_items_alphabetically()sort_items_alphabetically(), count_items()count_items(), todos a la vez. El resultado es una API abrumadora.
list_items_as_json()list_items_as_json()
Enfoque minimalista
add_item()add_item(), remove_item()remove_item(), list_items()list_items() (que devuelve una colección inmutable). Si
alguien necesita ordenar, filtra desde fuera. Si necesita JSON, usa serialización
estándar. La API es clara y pequeña.
Usabilidad: consistencia, descubribilidad y APIs difíciles de usar mal
Cuarto principio
Si el problema es que quien consume entienda rápidamente qué hace cada operación, entonces los nombres deben ser consistentes, los tipos deben evitar errores silenciosos y los errores deben ser informativos.
Una API es usable cuando quien la consume puede orientarse con poca fricción, descubrir sus operaciones principales y equivocarse difícilmente. Esto se logra con consistencia, nombres claros, tipos significativos y errores que realmente ayuden.
Las tres dimensiones de la usabilidad
- Consistencia: mantén patrones estables en nombres y política de retorno.
- Descubribilidad: los nombres deben sugerir qué hacen. Prefiere verbos claros y específicos del dominio (
transferFunds,registerAccount) antes que genéricos (exec,process). Si el IDE muestra autocompletado, debería ser fácil orientarse. - Difícil de usar mal: usa tipos para que el compilador rechace código incorrecto. Crea tipos específicos (
Email,Amount) en lugar de primitivos genéricos. Así quien consume no puede pasar datos inválidos ni en orden equivocado.
Ejemplo: orden de parámetros y tipos (usabilidad)
Imagina una función de transferencia.
def transfer(string_1, string_2, number, boolean): ...def transfer(
origin: Account,
destination: Account,
amount: Amount,
retry_on_failure: RetryPolicy
) -> TransferResult: ...Perspectiva de quien consume: diseñar para quien usará tu API
Quinto principio (transversal)
Todos los principios anteriores dicen: piensa en quien usa la API. Es el criterio más importante porque guía todas las decisiones. Modelar bien el dominio, proteger invariantes, limitar opciones y comunicar claramente solo tienen sentido si están pensados desde el punto de vista de quien le dará uso.
Este principio atraviesa a todos los demás: diseña la API pensando en quien la va a usar, no solo en quien la implementa. Eso implica priorizar claridad, anticipar errores comunes y contrastar el diseño con casos de uso reales.
Empatía con quien consume
Una API bien diseñada muestra empatía con quien la consume. Eso no significa complacer cualquier petición, sino entender necesidades reales, errores probables y el contexto en que otras personas usarán tu código.
Por ejemplo, si tu biblioteca maneja dinero, conviene anticipar confusiones entre unidades, errores frecuentes al construir montos o dudas sobre cómo manejar fallos. Los tipos, los nombres y los ejemplos deberían ayudar a evitar esas fricciones.
Tres criterios clave para aplicar esta perspectiva
- Haz fácil lo común, sin sacrificar lo importante: la operación más frecuente debería ser reconocible y requerir pocos pasos, pero los casos especiales deben tener respuestas claras.
- Errores que comunican claridad: si algo falla, el error debe explicar por qué sucedió y sugerir cómo reaccionar. No expongas detalles internos confusos ni códigos enigmáticos.
- Valida tu diseño con casos reales: antes de publicar, usa tu propia API. Implementa un caso de uso completo. ¿Fue intuitivo? ¿Fácil de extender? Si no, todavía hay deuda de diseño.
Caso de estudio: API de transferencias bancarias (usabilidad y manejo de errores)
Versión 1 (sin perspectiva de consumidor)
La API expone una funcióntransfer(origin: String, destination: String, amount: Double): Inttransfer(origin: String, destination: String, amount: Double): Int. Si falla, devuelve un código numérico (0, 1, 2, 3) que quien
consume debe memorizar o buscar en documentación. Además, DoubleDouble es una representación poco adecuada para dinero, porque introduce ambigüedad y
posibles errores de precisión. El resultado: errores difíciles de
diagnosticar y poco contexto para decidir cómo reaccionar.
Versión 2 (con perspectiva de consumidor)
La API expone . Los errores no son códigos ni strings, sino tipos específicos:
transfer(origin: String, destination: String, amount: Amount): Result<TransferId, TransferError>transfer(origin: String, destination: String, amount: Amount): Result<TransferId, TransferError>
-
InsufficientFunds { missingAmount: Amount }InsufficientFunds { missingAmount: Amount } -
AccountNotFound { accountId: AccountId }AccountNotFound { accountId: AccountId } -
NetworkError { retryable: Boolean }NetworkError { retryable: Boolean }
Cada tipo de error no solo comunica qué falló, sino que adjunta exactamente la información necesaria para decidir cómo reaccionar: cuánto falta, qué cuenta es inválida, si vale la pena reintentar.
Así, quien consume no solo sabe qué falló, sino que tiene información disponible para manejar cada caso. La API comunica mejor, reduce la dependencia de documentación externa y hace mucho más explícito el tratamiento de errores. En lenguajes con soporte adecuado, este diseño incluso puede ayudar a verificar de forma exhaustiva los casos que deben manejarse.
Conclusiones
Una API bien diseñada no expone primero detalles técnicos, sino conceptos del problema que resultan reconocibles para quien la usa. Por eso, modelar el dominio, encapsular invariantes y elegir con cuidado la superficie pública no son decisiones independientes: juntas determinan qué tan clara, segura y expresiva será la biblioteca.
La minimalidad útil, la usabilidad y la perspectiva de quien consume completan ese diseño. Una API pequeña pero suficiente, consistente en sus nombres y difícil de usar mal permite trabajar con mayor confianza, reduce errores evitables y vuelve más evidente la intención del código que la utiliza.
También conviene leer estos principios como un punto de partida para la conversación del curso, no como una discusión cerrada. Iremos retomándolos cuando aparezcan en contextos más específicos, de modo que cada uno gane matices a medida que el diseño de bibliotecas se vuelva más concreto.
Puntos clave
- Una buena API refleja los conceptos centrales del dominio en sus tipos, operaciones y relaciones.
- Encapsular no es solo ocultar implementación: también implica proteger invariantes y evitar estados inválidos.
- La minimalidad útil elimina lo accesorio sin dejar vacíos importantes en el problema que la biblioteca promete resolver.
- La usabilidad depende de consistencia, descubribilidad, tipos significativos y errores que orienten con claridad.
- Diseñar desde la perspectiva de quien consume obliga a validar la API con casos reales, defaults razonables y flujos de uso concretos.
Reflexión de cierre
Diseñar bibliotecas no es acumular funcionalidades, sino tomar decisiones sobre qué conceptos merecen existir en la API, qué errores deberían prevenirse desde el diseño y qué experiencia tendrá la persona que la use. Una API realmente lograda no solo funciona: también enseña, orienta y hace más natural resolver el problema para el que fue creada.
A lo largo del curso, estos cinco principios reaparecerán en contextos más específicos: cuando entres en decisiones de modelado de datos, cuando documentes APIs, cuando tengas que pensar en evolución y compatibilidad, y cuando diseñes herramientas que quien desarrolla usa directamente. Cada contexto los enriquecerá con nuevos matices. Por ahora, lo importante es contar con estos términos para nombrar decisiones bien y reconocer estos problemas cuando surjan.
¿Con ganas de más?
Referencias recomendadas
- Esta lectura puede interesarte si quieres profundizar varios de los principios presentados en la lección desde una perspectiva de diseño más detallada. Reddy desarrolla con mayor calma ideas como modelar el dominio, elegir una abstracción que realmente responda al problema, identificar los objetos clave, ocultar detalles de implementación y reducir el acoplamiento. Resulta especialmente útil para pasar de la intuición general de «una buena API debe ser clara y fácil de usar» a criterios más concretos para evaluar la superficie pública de una biblioteca.
Referencias adicionales
- Una vez comprendida la importancia de la perspectiva de quien consume, Lauret ofrece un marco más operativo para llevarla al diseño real. Este capítulo profundiza en cómo traducir la empatía en decisiones concretas: versionado, evolución sin ruptura, manejo significativo de errores y patrones de compatibilidad. Aunque el foco está en APIs REST y HTTP, los principios de consistencia, descubribilidad y comunicación clara que desarrolla son igualmente valiosos para cualquier interfaz de biblioteca.
- “Functions” (pp. 78–106) en Clean code collection por Robert C. MartinEste capítulo complementa muy bien lo aprendido sobre minimalidad útil y usabilidad. Martin muestra de manera concreta cómo refactorizar funciones confusas hasta convertirlas en funciones que hacen una sola cosa, la hacen bien y solo esa cosa. Sus criterios sobre tamaño, nivel de abstracción, nombres descriptivos y parámetros se conectan directamente con el diseño de APIs: una función pública con demasiados argumentos o con varios niveles de abstracción resulta más difícil de usar, y un tipo con demasiados métodos tampoco refleja una interfaz minimalista. Si después de esta lección te preguntas cómo refactorizar los métodos de una API para volverlos más pequeños, claros y fáciles de testear, aquí encontrarás criterios concretos y ejemplos de transformación paso a paso.
- “Meaningful Names” (pp. 60–77) en Clean code collection por Robert C. MartinEste capítulo es especialmente útil para llevar a la práctica dos principios de la lección: modelar el dominio y construir una usabilidad más clara. Martin explica en profundidad por qué los nombres importan tanto en el código: cómo elegir nombres que revelen intención, eviten desinformación, se pronuncien con naturalidad y puedan buscarse sin ambigüedad. Sus criterios conectan directamente con aquello que vuelve a una API fácil de entender y difícil de usar mal. Si después de esta lección dudas sobre cómo nombrar un tipo, un parámetro o un método, este capítulo ofrece principios concretos que van más allá de la intuición o la preferencia personal.