Library Development Patterns
Foundational patterns for developing reusable library code across programming languages. This meta-skill provides guidance that language-specific library skills (e.g., rust-lib-dev, python-lib-dev) extend.
When to Use This Skill
- •Designing library public APIs
- •Organizing modules and package structure
- •Managing semantic versioning and compatibility
- •Creating utility libraries, frameworks, or toolkits
- •Establishing documentation and testing standards for libraries
- •Publishing and distributing packages
This Skill Does NOT Cover
- •SDK development (wrapping APIs/services) - see
meta-sdk-patterns-eng - •Application architecture - see
architecture-patterns - •Language-specific implementation details - see
*-lib-*skills - •CLI tool development - see
*-cli-*skills
Library vs SDK: Key Distinctions
| Aspect | Library | SDK |
|---|---|---|
| Purpose | Reusable functionality | API/service wrapper |
| Dependencies | Minimal, self-contained | Tied to external service |
| Versioning | Independent semver | Often tracks API version |
| Examples | lodash, requests, serde | AWS SDK, Stripe SDK |
| Testing | Unit/integration tests | Mocks external service |
Public API Design
Principle: Minimal Surface Area
Expose only what users need. Everything public becomes a maintenance commitment.
Good: 3-5 primary exports that compose Bad: 50 exports that users must navigate
Export Patterns
Layered Exports:
library/
├── mod.rs (or index.ts, __init__.py) # Primary public API
├── types.rs # Public types/interfaces
├── errors.rs # Error types
└── internal/ # Private implementation
└── ...
Explicit vs Glob Exports:
# Preferred: Explicit exports from .core import parse, validate, transform __all__ = ["parse", "validate", "transform"] # Avoid: Glob re-exports (hides what's public) from .core import *
Naming Conventions
| Element | Convention | Example |
|---|---|---|
| Functions | Verb-first, descriptive | parse_json, validateInput |
| Types/Classes | Noun, singular | Parser, ValidationResult |
| Constants | SCREAMING_SNAKE | DEFAULT_TIMEOUT, MAX_RETRIES |
| Modules | Lowercase, descriptive | parser, validators |
Function Signatures
Prefer Specific Over Generic:
// Good: Clear intent fn parse_config(path: &Path) -> Result<Config, ParseError> // Avoid: Too generic, unclear fn parse(input: &str) -> Result<Value, Error>
Parameter Guidelines:
- •0-3 parameters: Use positional
- •4+ parameters: Use options object/struct
- •Required first, optional last
- •Sensible defaults for optional parameters
// Good: Options object for many parameters
interface ParseOptions {
encoding?: string;
strict?: boolean;
maxDepth?: number;
}
function parse(input: string, options?: ParseOptions): Result
// Avoid: Too many positional parameters
function parse(input: string, encoding: string, strict: boolean, maxDepth: number): Result
Module Organization
Standard Structure
library/ ├── src/ │ ├── lib.rs # Rust: Library root │ ├── index.ts # TS: Package entry │ ├── __init__.py # Python: Package init │ │ │ ├── core/ # Core functionality │ │ ├── mod.rs │ │ ├── parser.rs │ │ └── validator.rs │ │ │ ├── types/ # Type definitions │ │ ├── mod.rs │ │ └── config.rs │ │ │ ├── errors/ # Error types │ │ ├── mod.rs │ │ └── parse_error.rs │ │ │ └── utils/ # Internal utilities │ └── ... │ ├── tests/ # Integration tests ├── examples/ # Usage examples ├── benches/ # Benchmarks (if applicable) └── docs/ # Additional documentation
Module Cohesion Principles
High Cohesion:
- •Modules should do one thing well
- •Related functionality stays together
- •Clear boundaries between modules
Low Coupling:
- •Modules should be independently testable
- •Minimize cross-module dependencies
- •Use dependency injection for flexibility
Re-export Patterns
Facade Pattern for Complex Libraries:
// lib.rs - Clean public interface
pub use crate::core::{parse, validate};
pub use crate::types::{Config, Result};
pub use crate::errors::ParseError;
// Internal modules hidden
mod core;
mod types;
mod errors;
mod utils; // Not re-exported
Versioning Strategy
Semantic Versioning (SemVer)
MAJOR.MINOR.PATCH MAJOR: Breaking changes MINOR: New features (backward compatible) PATCH: Bug fixes (backward compatible)
What Constitutes Breaking Changes?
| Change Type | Breaking? | Version Bump |
|---|---|---|
| Remove public function | Yes | MAJOR |
| Change function signature | Yes | MAJOR |
| Add required parameter | Yes | MAJOR |
| Add optional parameter | No | MINOR |
| Add new function | No | MINOR |
| Fix bug in existing behavior | No | PATCH |
| Performance improvement | No | PATCH |
Pre-release Versions
0.x.y - Initial development (breaking changes allowed in minor) 1.0.0-alpha.1 - Alpha release 1.0.0-beta.1 - Beta release 1.0.0-rc.1 - Release candidate 1.0.0 - Stable release
Deprecation Process
- •Announce deprecation with
@deprecatedannotation - •Document migration path in deprecation notice
- •Maintain deprecated API for at least one minor version
- •Remove in next major version
import warnings
def old_function():
"""
.. deprecated:: 2.0.0
Use :func:`new_function` instead.
"""
warnings.warn(
"old_function is deprecated, use new_function instead",
DeprecationWarning,
stacklevel=2
)
return new_function()
Error Handling
Error Type Design
Hierarchical Error Types:
#[derive(Debug, thiserror::Error)]
pub enum LibraryError {
#[error("Parse error: {0}")]
Parse(#[from] ParseError),
#[error("Validation error: {0}")]
Validation(#[from] ValidationError),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
}
#[derive(Debug, thiserror::Error)]
pub enum ParseError {
#[error("Invalid syntax at line {line}: {message}")]
InvalidSyntax { line: usize, message: String },
#[error("Unexpected token: {0}")]
UnexpectedToken(String),
}
Error Messages
Good Error Messages Include:
- •What happened
- •Where it happened (context)
- •How to fix it (when possible)
# Good ParseError: Invalid JSON at line 42: expected ',' or '}' after object property # Bad Error: Parse failed
Result Types
Prefer Result Types Over Exceptions (where language supports):
// Rust: Result type
pub fn parse(input: &str) -> Result<Document, ParseError>
// TypeScript: Discriminated union
type ParseResult =
| { success: true; data: Document }
| { success: false; error: ParseError };
Configuration Patterns
Configuration Hierarchy
1. Defaults (in code) 2. Config file 3. Environment variables 4. Explicit parameters
Builder Pattern for Configuration
let config = ConfigBuilder::new()
.timeout(Duration::from_secs(30))
.max_retries(3)
.strict_mode(true)
.build()?;
Validation at Construction
@dataclass
class Config:
timeout: int
max_retries: int
def __post_init__(self):
if self.timeout <= 0:
raise ValueError("timeout must be positive")
if self.max_retries < 0:
raise ValueError("max_retries cannot be negative")
Testing Strategies
Test Pyramid for Libraries
/\
/ \ E2E (examples work)
/----\
/ \ Integration (modules work together)
/--------\
/ \ Unit (individual functions work)
--------------
Testing Patterns
Property-Based Testing:
#[quickcheck]
fn parse_roundtrip(input: ValidJson) -> bool {
let parsed = parse(&input.0)?;
let serialized = serialize(&parsed);
input.0 == serialized
}
Snapshot Testing:
test('parser output', () => {
const result = parse(complexInput);
expect(result).toMatchSnapshot();
});
Fuzz Testing:
#[fuzz]
fn fuzz_parser(data: &[u8]) {
// Should not panic on any input
let _ = parse(data);
}
Example-Driven Testing
def parse_date(s: str) -> date:
"""Parse a date string.
Examples:
>>> parse_date("2024-01-15")
date(2024, 1, 15)
>>> parse_date("invalid")
Traceback (most recent call last):
...
ValueError: Invalid date format
"""
Documentation Standards
Documentation Hierarchy
README.md # Quick start, installation docs/ ├── guide/ # Tutorials, how-tos ├── reference/ # API documentation └── examples/ # Runnable examples
API Documentation Requirements
Every public item needs:
- •Summary - One-line description
- •Description - Detailed explanation (if needed)
- •Parameters - Each parameter documented
- •Returns - Return value documented
- •Errors - Possible errors documented
- •Examples - At least one usage example
/// Parses a configuration file into a Config struct.
///
/// Reads the file at the given path and parses it as TOML format.
/// The file must exist and be valid UTF-8.
///
/// # Arguments
///
/// * `path` - Path to the configuration file
///
/// # Returns
///
/// A `Config` struct containing the parsed configuration.
///
/// # Errors
///
/// Returns `ParseError::Io` if the file cannot be read.
/// Returns `ParseError::InvalidSyntax` if the TOML is malformed.
///
/// # Examples
///
/// ```
/// let config = parse_config("config.toml")?;
/// assert_eq!(config.timeout, 30);
/// ```
pub fn parse_config(path: &Path) -> Result<Config, ParseError> {
// ...
}
README Template
# Library Name Brief description of what the library does. ## Installation \`\`\`bash # Package manager command \`\`\` ## Quick Start \`\`\`language // Minimal working example \`\`\` ## Features - Feature 1 - Feature 2 - Feature 3 ## Documentation - [Guide](docs/guide/) - [API Reference](docs/reference/) - [Examples](examples/) ## License [License type]
Compatibility Considerations
Minimum Supported Version
Document and test against minimum versions:
# Cargo.toml
rust-version = "1.70"
# pyproject.toml
requires-python = ">=3.9"
# package.json
"engines": {
"node": ">=18.0.0"
}
Feature Flags
Use feature flags for optional functionality:
# Cargo.toml [features] default = ["json"] json = ["serde_json"] yaml = ["serde_yaml"] async = ["tokio"]
Backward Compatibility Strategies
- •Additive changes only in minor versions
- •Feature flags for optional new features
- •Type aliases to rename types without breaking
- •Default parameters instead of signature changes
Publishing and Distribution
Pre-publish Checklist
- • Version bumped appropriately
- • CHANGELOG updated
- • All tests passing
- • Documentation updated
- • Examples work with new version
- • No accidental breaking changes
- • License file present
- • README accurate
Registry-Specific Guidelines
| Registry | Key Requirements |
|---|---|
| crates.io | Cargo.toml metadata complete, license |
| npm | package.json fields, TypeScript types |
| PyPI | pyproject.toml, classifiers, requires-python |
| Maven | pom.xml, GPG signing, Javadoc |
Changelog Format
Follow Keep a Changelog:
# Changelog ## [Unreleased] ## [2.1.0] - 2024-01-15 ### Added - New `parse_strict` function for strict parsing ### Changed - Improved error messages for parse failures ### Deprecated - `parse_loose` is deprecated, use `parse` with `strict: false` ## [2.0.0] - 2024-01-01 ### Changed - **BREAKING**: Renamed `Config` to `Settings` - **BREAKING**: `parse` now returns `Result` instead of panicking
Performance Considerations
Avoid Premature Optimization
- •Correctness first - Make it work
- •Clarity second - Make it readable
- •Performance third - Make it fast (if needed)
Common Performance Patterns
Zero-Copy Where Possible:
// Good: Borrow instead of clone fn process(data: &str) -> Result<&str, Error> // Avoid: Unnecessary allocation fn process(data: &str) -> Result<String, Error>
Lazy Evaluation:
// Good: Iterator (lazy) fn items(&self) -> impl Iterator<Item = &Item> // Avoid: Collect everything (eager) fn items(&self) -> Vec<Item>
Benchmark Before Optimizing:
#[bench]
fn bench_parse(b: &mut Bencher) {
let input = include_str!("../fixtures/large.json");
b.iter(|| parse(input));
}
Extension Points
Plugin Architecture
pub trait Plugin {
fn name(&self) -> &str;
fn process(&self, input: &Document) -> Result<Document, Error>;
}
pub struct Library {
plugins: Vec<Box<dyn Plugin>>,
}
impl Library {
pub fn register_plugin(&mut self, plugin: impl Plugin + 'static) {
self.plugins.push(Box::new(plugin));
}
}
Callback Hooks
interface Hooks {
beforeParse?: (input: string) => string;
afterParse?: (result: Document) => Document;
onError?: (error: Error) => void;
}
function parse(input: string, hooks?: Hooks): Document {
const processed = hooks?.beforeParse?.(input) ?? input;
// ...
}
Anti-Patterns to Avoid
1. God Module
Problem: Single module with too many responsibilities Solution: Split by domain/functionality
2. Leaky Abstractions
Problem: Internal implementation details exposed in public API Solution: Clear public/private boundaries
3. Unstable Dependencies
Problem: Depending on unstable/pre-1.0 libraries in public API Solution: Wrap or re-export with stable types
4. Breaking Changes in Patches
Problem: Changing behavior in patch releases Solution: Strict semver adherence
5. Missing Error Context
Problem: Generic errors that don't help debugging Solution: Rich error types with context
6. Documentation Drift
Problem: Docs out of sync with code Solution: Doc tests, examples in tests
References
- •Semantic Versioning
- •Keep a Changelog
- •Rust API Guidelines
- •The Little Manual of API Design
- •Language-specific:
*-lib-*skills - •SDK development:
meta-sdk-patterns-eng