APIs that cross module boundaries, wire formats, or event streams should be designed so callers can’t accidentally construct invalid requests or rely on unstable semantics.
Apply these rules: 1) Prefer request objects/enums over ambiguous parameters
(Option<T>, Option<U>) when only certain combinations are valid.ReportShutdownRequest) or split into distinct methods.Example:
async fn report_shutdown(&self, req: ReportShutdownRequest) -> Result<()> {
// handle only valid states; unreachable invalid combinations
}
// vs:
// async fn report_shutdown(&self, error_category: Option<String>, error_message: Option<String>) -> Result<()>;
2) Use named fields (and owned inputs) for client interfaces
struct parameters with named fields so arguments can’t be swapped.String) over &str for async/client boundaries unless there’s a strong lifetime reason.Example:
pub struct InitializeRequest {
pub user_id: String,
pub user_email: String,
pub crash_reporting_enabled: bool,
}
pub async fn initialize(&self, auth_token: Option<String>, req: InitializeRequest) -> Result<()>;
3) Evolve event/wire contracts compatibly
4) Be cautious with serialization changes
5) Keep public API surfaces intentional
These practices reduce misuse at the boundary, make intent obvious at call sites, and prevent subtle regressions when contracts evolve.
Enter the URL of a public GitHub repository