Saltar al contenido principal

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

Cambios recientes:

  • 11c4fd1 · 7 de abril de 2026 · ✨♻️ feat(notes): add API design fundamentals and modular bibliography catalog ( GitLab / GitHub )

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 Account sobre HashMap.
  • 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

El dominio modela no solo datos, sino también comportamiento. Operaciones como transfer(), validate(), 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 como 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 como Country, PostalCode y Address, con validaciones encapsuladas. Así, quien consume construye una Address 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 String vuelve más fácil confundir campos, aceptar combinaciones inválidas o postergar reglas importantes hasta muy tarde. Tipos como Country, PostalCode o Address permiten expresar mejor el dominio y hacer que ciertos errores sean más difíciles de cometer.

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., Account no es solo un número; es un concepto con historial, propietario, estado.
  • Necesita métodos especializados: p. ej., Address podría tener normalize() o 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 account.balance = -100, rompiendo el invariante.

Solución

Usa un constructor privado, una factory privada, y métodos públicos que verifiquen invariantes antes de modificar el estado. Ahora la biblioteca garantiza que ningún saldo será nunca negativo, y puedes cambiar internamente cómo almacenas o calculas el saldo sin romper código externo.

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 get_balance(), evita sumar variantes como fetch_balance(), consult_balance() o balance_getter() sin una diferencia real de comportamiento.
  • 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 create_from_parts(country, postal_code, street) y además ofrecer una conveniencia como parse_address(raw_address) 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.

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(), sort_items_alphabetically(), count_items(), list_items_as_json(), todos a la vez. El resultado es una API abrumadora.

Enfoque minimalista

Expone solo add_item(), remove_item(), 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.

Versión confusa
def transfer(string_1, string_2, number, boolean): ...
¿Qué es cada parámetro? ¿Cuál es el origen y cuál el destino? ¿El número es el monto o la cantidad de intentos? ¿El booleano activa un registro, fuerza la operación o cambia otra regla de negocio? Este tipo de firma obliga a recordar demasiado y vuelve la API más fácil de usar mal.
Versión clara
def transfer(
    origin: Account,
    destination: Account,
    amount: Amount,
    retry_on_failure: RetryPolicy
) -> TransferResult: ...
Aquí la intención es visible. Los tipos reducen errores posibles, el orden tiene significado semántico, y el nombre de cada parámetro sugiere claramente su propósito.

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ón transfer(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, Double 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 transfer(origin: String, destination: String, amount: Amount): Result<TransferId, TransferError>. Los errores no son códigos ni strings, sino tipos específicos:

  • InsufficientFunds { missingAmount: Amount }
  • AccountNotFound { accountId: AccountId }
  • 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 adicionales

  • “Designing an API for its users” (pp. 17–42) en The design of web APIs por Arnaud Lauret
    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. Martin
    Este 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. Martin
    Este 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.