8.1 KiB
Example: API Review
Review of a proposed media player store API.
Target
// Proposed API
function createPlayer(
source: string,
autoplay: boolean,
muted: boolean,
controls: ControlsConfig,
plugins?: Plugin[]
): Player;
interface Player {
play(): void;
pause(): void;
getState(): PlayerState;
registerPlugin(plugin: Plugin): void;
on(event: string, handler: Function): void;
}
API Review: createPlayer
Overall Score: 4/10
| Dimension | Score | Critical | Major | Minor |
|---|---|---|---|---|
| Types | 4/10 | 0 | 2 | 0 |
| API Surface | 3/10 | 1 | 1 | 1 |
| Extensibility | 3/10 | 1 | 1 | 0 |
| Progressive Disclosure | 6/10 | 0 | 1 | 1 |
Critical Issues
[CRITICAL] Function uses 5 positional parameters
What: createPlayer(source, autoplay, muted, controls, plugins) requires remembering argument order
Where: createPlayer() function signature
Why: Impossible to remember order; adding options requires breaking changes; boolean params particularly confusing (true, false means what?)
Principle: Config objects for 3+ params (principles.md)
Fix: Single config object
// Before
createPlayer('video.mp4', true, false, defaultControls, [analytics]);
// After
createPlayer({
src: 'video.mp4',
autoplay: true,
controls: defaultControls,
features: [analyticsFeature],
});
[CRITICAL] Runtime plugin registration loses type safety
What: player.registerPlugin(plugin) allows adding plugins after creation
Where: Player.registerPlugin() method
Why: TypeScript can't track what capabilities exist; ordering is implicit; can't tree-shake unused plugins
Principle: Emergent extensibility through composition (principles.md)
Fix: Composition at creation time
// Before
const player = createPlayer(config);
player.registerPlugin(analytics); // Types don't know analytics exists
player.registerPlugin(keyboard); // Order matters but isn't visible
// After
const player = createPlayer({
...config,
features: [analyticsFeature, keyboardFeature], // Types know exactly what's included
});
Major Issues
[MAJOR] getState() returns full state on every call
What: getState() likely returns a new object each time
Where: Player.getState(): PlayerState
Why: React components re-render on every state change, not just relevant changes
Principle: Selectors for fine-grained subscriptions (state.md)
Fix: Support selector pattern
// Before
const state = player.getState(); // New object every time
const paused = state.paused; // Component re-renders on ANY change
// After
const paused = usePlayer((s) => s.paused); // Re-render only when paused changes
[MAJOR] Event handler uses untyped string events
What: on(event: string, handler: Function) has no type safety
Where: Player.on() method
Why: No autocomplete for event names; handler arguments untyped; typos fail silently
Principle: Types as contracts (typescript.md)
Fix: Typed event map
// Before
player.on('play', (e) => {}) // 'play' could be typo, e is any
player.on('plaay', (e) => {}) // Silent failure
// After
interface PlayerEvents {
play: { time: number }
pause: { time: number }
ended: {}
}
player.on('play', (e) => { // Autocomplete, e is typed
console.log(e.time)
})
player.on('plaay', ...) // TS Error: 'plaay' not in PlayerEvents
[MAJOR] No escape hatch to underlying element
What: API doesn't expose access to raw video/audio element
Where: Player interface (missing)
Why: Power users can't handle edge cases (custom codecs, WebRTC, canvas capture)
Principle: Escape hatches that compose (principles.md)
Fix: Explicit escape hatch
// After
interface Player {
// ... primary API ...
// Escape hatch (named to signal "you're on your own")
get __unsafe__(): {
mediaElement: HTMLMediaElement;
audioContext?: AudioContext;
};
}
Minor Issues
| Location | Issue | Principle | Fix |
|---|---|---|---|
autoplay: boolean |
Boolean params confusing | principles.md | Named in config object |
controls: Config |
Generic name | principles.md | Consider ui or skin |
plugins?: Plugin[] |
Plugin vs Feature naming | state.md | Use "feature" if that's model |
Good Patterns Found
- Clear method names:
play(),pause(),getState()are intuitive - Separation of concerns: Player vs state distinction is present
- Optional plugins: Not required for basic usage
Summary
This API has structural problems that will cause long-term pain. The two critical issues—positional parameters and runtime plugin registration—should be addressed before any public release.
Positional parameters make the API hard to use and impossible to extend without breaking changes. Converting to a config object is straightforward and enables future options.
Runtime plugin registration loses the type safety and tree-shaking benefits that modern libraries expect. Moving to creation-time composition (like Zustand features) enables TypeScript to track capabilities and bundlers to eliminate unused code.
The event system's lack of typing is a significant DX issue but not blocking. Consider typed event maps or a subscription pattern like subscribe(selector, callback).
Priority order:
- Convert to config object (blocks everything else)
- Move plugins to creation-time composition
- Add typed events
- Add selector-based subscriptions
- Add escape hatches
Full Types Review
Types Review
Score: 4/10
Issues
[MAJOR] Untyped events
(See main report)
[MAJOR] Plugin type doesn't carry capabilities
Plugin interface doesn't encode what state/methods the plugin adds, so TypeScript can't know what's available after registration.
Good Patterns
- Player interface is defined
- State type exists (PlayerState)
Summary
The foundation is there but the dynamic parts (events, plugins) bypass the type system entirely. Consider making these static/creation-time.
Full API Surface Review
API Surface Review
Score: 3/10
Issues
[CRITICAL] Positional parameters
(See main report)
[MAJOR] getState() performance
(See main report)
[MINOR] Boolean parameters
Two adjacent booleans (autoplay, muted) are confusing at call sites.
Good Patterns
- Method names are clear and conventional
- Return type is defined (Player interface)
Summary
The function signature is the main problem. Converting to a config object would immediately improve usability and enable type inference for options.
Full Extensibility Review
Extensibility Review
Score: 3/10
Issues
[CRITICAL] Runtime registration
(See main report)
[MAJOR] No composition model
Plugins are black boxes. No feature pattern, no middleware composition, no builder chain.
Good Patterns
- Plugin concept exists (just needs different delivery)
Summary
The extensibility model needs a rethink. Look at Zustand features or tRPC procedures for inspiration—extension through composition at creation time.
Full Progressive Disclosure Review
Progressive Disclosure Review
Score: 6/10
Issues
[MAJOR] No escape hatch
(See main report)
[MINOR] All-or-nothing controls
controls: ControlsConfig is required but users might want headless or partial UI.
Good Patterns
- Basic usage is simple (source, play/pause)
- Plugins are optional
Summary
The layering is reasonable but escape hatches are missing. Power users have no path to lower levels without abandoning the library.