Subchapter 26.1
references/canonical_api_design.mdMarkdown9 KBView on GitHub
This document contains the canonical principles for designing and improving APIs within the GenUI ecosystem. These principles are distilled from established software engineering best practices.
Foundational decisions that shape the system’s structure and maintainability.
Treat the interface definition as the primary product artifact, distinct from its implementation. Define the contract (the “what”) rigorously before writing the code (the “how”). This decoupling allows consumers to validate the design against requirements before engineering resources are committed, and it serves as the single source of truth for the system’s capabilities.
Complexity is the primary vector for defects and integration friction. Expose the smallest number of concepts necessary to solve the problem, aiming for “just enough power.” Avoid “Swiss Army knife” interfaces that perform multiple unrelated operations. As a useful heuristic, if an interface requires extensive setup or explanation to use, it likely violates this principle.
Do not add parameters, operations, or data fields for hypothetical future needs. Speculative features introduce “accidental complexity”—complexity arising from the solution rather than the problem. Aim to build only what is required for current, concrete use cases to maintain a lean and testable interface.
The API must expose capabilities, not internal state or storage models. Never leak implementation details, such as database schemas or specific algorithms, through the interface. This encapsulation ensures that the internal architecture can be refactored or completely replaced without breaking consumers.
Prefer explicit parameter passing and dependency injection over stateful global state, singletons, registries, or environment variables for API configuration. Implicit configurations obscure dependencies from caller visibility, prevent static type safety, and introduce global state mutations that make concurrent execution and parallel testing difficult. Programmatic instantiations are statically verifiable, isolate test cases, and make dependency compilation boundaries transparent to bundling tools.
Principles for ensuring the API is predictable, descriptive, and easy to use.
An interface should behave exactly as a user expects based on their prior knowledge of the system. If a standard pattern exists for naming conventions or error structures, deviate from it only with significant justification. Consistency reduces the cognitive load required to learn and use the API.
Names should describe business intent rather than technical mechanics. For example, a method named publishArticle() is superior to updateRowStatus(). Use specific, self-descriptive terminology, like temperatureCelsius instead of temp, to prevent ambiguity and reduce reliance on external documentation.
Clearly distinguish between operations that retrieve data (queries) and operations that modify state (commands). Queries should ideally be safe and side-effect-free, allowing consumers to call them repeatedly without risk. Avoid mixing data retrieval with state mutation in a single operation, as it leads to unpredictable system behavior.
Design small, independent primitives that can be composed to create complex behaviors, rather than creating rigid, complex operations for every specific scenario. Changing one independent parameter should not have unexpected side effects on unrelated parts of the request.
Designing for failure, distributed systems, and performance in a production environment.
In a distributed system, network failures are inevitable. Operations that change state must be designed to be idempotent, meaning they can be safely retried multiple times without producing cumulative side effects, such as double-charging. This is often achieved by requiring clients to provide a unique idempotency key or transaction identifier.
Follow the robustness principle: “Be conservative in what you send, be liberal in what you accept.” Strictly adhere to the interface contract when generating output, but implement tolerance when processing input, such as ignoring unknown fields rather than crashing. This approach improves interoperability and allows the system to evolve without immediately breaking older clients.
If a dependency or subsystem is failing, stop calling it immediately to prevent cascading failures. The interface should fail fast and return a specific error rather than hanging indefinitely. This preserves system resources and provides immediate feedback to the consumer.
To reduce network overhead and latency, provide mechanisms to process multiple items in a single request, such as createItems([item1, item2]). Ensure the behavior of partial failures is well-defined: either the entire batch fails atomically, or the response clearly indicates the success or failure status of each individual item.
Guidelines for managing state, validation, and long-term interface maintenance.
Never return unbounded lists of data. For large datasets, prefer cursor-based (keyset) pagination over offset-based pagination. Cursors, which point to a specific record, provide constant-time performance (O(1)) and data stability. In contrast, offsets degrade in performance (O(n)) and suffer from skipped or duplicate records when data is modified during iteration.
Plan for change from day one. Interfaces will evolve, and breaking changes must be managed without disrupting existing consumers. Use explicit versioning strategies, such as semantic versioning, and establish a clear deprecation lifecycle (warn, then sunset, and finally remove) to give consumers time to migrate.
Enforce constraints at the boundary. Use strong types, such as enums instead of raw strings, or distinct types for monetary values, to make invalid states unrepresentable. Validate all input rigorously before processing to prevent data corruption and security vulnerabilities.
Errors must be machine-readable and actionable. Return specific error codes, rather than generic failure indicators, accompanied by descriptive messages. Where possible, include structured metadata that allows the consumer to programmatically recover or correct their input.
Avoid defining the same business logic or data structure in multiple places. Ensure that a concept, such as a user or an order, has a canonical representation. Divergent definitions lead to integration errors where one part of the system enforces rules that another ignores.