agpm-cli 0.4.14

AGent Package Manager - A Git-based package manager for coding agents
Documentation
---
name: api-design-principles
description: Best practices for designing clean, maintainable APIs
tags: [api, design, best-practices]
---

# API Design Principles

## General Principles

### 1. Consistency is King
- Use consistent naming conventions across the API
- Follow established patterns from the language/framework
- Be predictable in structure and behavior

### 2. Make the Simple Easy, the Complex Possible
- Common use cases should require minimal code
- Advanced features should be available but not in the way
- Provide sensible defaults

### 3. Design for Evolution
- Plan for future additions without breaking changes
- Use extensible data structures
- Consider versioning strategy from the start

## Naming Conventions

### Be Descriptive and Unambiguous
```rust
// Good
fn parse_config_file(path: &Path) -> Result<Config>

// Bad
fn parse(path: &Path) -> Result<Config>
fn get_config(path: &Path) -> Result<Config>  // "get" is vague
```

### Use Domain Language
```rust
// Good - uses domain terminology
struct ShoppingCart {
    fn add_item(&mut self, item: Item)
    fn checkout(&self) -> Order
}

// Bad - generic terminology
struct Container {
    fn add(&mut self, thing: Thing)
    fn process(&self) -> Result
}
```

## Function Design

### Single Responsibility
```rust
// Good - each function does one thing
fn load_config(path: &Path) -> Result<String>
fn parse_config(content: &str) -> Result<Config>

// Bad - does too much
fn load_and_parse_and_validate_config(path: &Path) -> Result<Config>
```

### Avoid Boolean Parameters
```rust
// Good - explicit and self-documenting
enum CacheStrategy {
    UseCache,
    BypassCache,
}
fn fetch_data(url: &str, cache: CacheStrategy) -> Result<Data>

// Bad - unclear what true/false means
fn fetch_data(url: &str, use_cache: bool) -> Result<Data>
```

### Return Meaningful Types
```rust
// Good - uses type system for safety
fn get_user(id: UserId) -> Result<User, UserError>

// Bad - stringly-typed errors
fn get_user(id: u64) -> Result<User, String>
```

## Error Handling

### Provide Context
```rust
#[derive(Debug, thiserror::Error)]
pub enum ConfigError {
    #[error("Failed to read config file at {path}: {source}")]
    ReadError {
        path: PathBuf,
        source: io::Error,
    },

    #[error("Invalid configuration: {0}")]
    ValidationError(String),
}
```

### Distinguish Recoverable from Unrecoverable
```rust
// Recoverable - returns Result
fn connect_to_server(addr: &str) -> Result<Connection, ConnectError>

// Unrecoverable - panics
fn initialize() {
    let config = load_config()
        .expect("Config must exist for application to run");
}
```

## Type Design

### Use Newtypes for Type Safety
```rust
// Good - impossible to mix up different IDs
pub struct UserId(u64);
pub struct OrderId(u64);

fn process_order(order_id: OrderId) -> Result<()>

// Bad - easy to pass wrong ID
fn process_order(order_id: u64) -> Result<()>
```

### Leverage the Builder Pattern
```rust
// Good - clear and flexible
let client = HttpClient::builder()
    .timeout(Duration::from_secs(30))
    .retry_count(3)
    .build()?;

// Bad - too many parameters
let client = HttpClient::new(
    "https://api.example.com",
    Duration::from_secs(30),
    3,
    true,
    false,
    None,
)?;
```

## Async API Design

### Be Clear About Async Boundaries
```rust
// Good - async is visible in signature
pub async fn fetch_data(url: &str) -> Result<Data>

// Also good - returns Future explicitly
pub fn fetch_data(url: &str) -> impl Future<Output = Result<Data>>
```

### Avoid Blocking in Async
```rust
// Good - uses async I/O
pub async fn read_file(path: &Path) -> Result<String> {
    tokio::fs::read_to_string(path).await
}

// Bad - blocks the executor
pub async fn read_file(path: &Path) -> Result<String> {
    std::fs::read_to_string(path)  // Blocking!
}
```

## Documentation

### Document Invariants and Contracts
```rust
/// Parses a configuration file.
///
/// # Arguments
/// * `path` - Path to the config file (must exist)
///
/// # Returns
/// The parsed configuration, or an error if:
/// - The file cannot be read
/// - The file contains invalid TOML
/// - Required fields are missing
///
/// # Example
/// ```no_run
/// let config = parse_config(Path::new("config.toml"))?;
/// ```
pub fn parse_config(path: &Path) -> Result<Config>
```

### Document Panics
```rust
/// Gets the user with the given ID.
///
/// # Panics
/// Panics if the database connection pool is exhausted.
pub fn get_user(&self, id: UserId) -> Result<User>
```

## Versioning

### Semantic Versioning
- MAJOR: Breaking changes
- MINOR: New features (backwards compatible)
- PATCH: Bug fixes

### Deprecation Strategy
```rust
/// Gets the user email.
///
/// # Deprecated
/// Use [`User::email_address`] instead. This method will be
/// removed in version 2.0.0.
#[deprecated(since = "1.5.0", note = "Use email_address instead")]
pub fn get_email(&self) -> &str {
    &self.email
}
```