Skill: Craft API
"The best API is the one developers already know how to use before reading the docs."
The Standard
One Obvious Entry Point: Every package has one clear "start here." Never two equally-valid paths to the same thing.
Style Example When Trait method $model->addMedia($file)Package augments a model Static factory QueryBuilder::for(User::class)Package is standalone ::make()schema builderTextInput::make('name')Declarative UI/component system (Filament) Manager / driver Notification::driver('slack')Multi-implementation service Helper function activity()->log('...')Universal utility, used everywhere Constructor new UserData(name: 'John')Value objects, DTOs Fluent Builders: Configuration methods return
$this. Terminal methods return the result. Configuration accumulates, execution happens once. The terminal method name makes the action explicit.Terminal Method Pattern: Configure, then execute. The builder is inert until the terminal method fires. Never mix configuration with execution.
Progressive Disclosure: Four layers. Each additive.
- Layer 1 -- Zero-config: Works immediately. One method call.
- Layer 2 -- Common customization: Named collections, custom properties, guard specification.
- Layer 3 -- Power user: Custom filters, responsive images, batch logging. Full control.
- Layer 4 -- Framework extension: Implement contracts, extend base classes, swap implementations.
Named Constructors: Static factory methods for value objects and declarations. Every common case gets its own factory.
AllowedFilter::exact(),::partial(),::scope(),::callback().ConditionableandTappableTraits: AddConditionableto builders for fluent conditional logic (when(),unless()) without breaking method chains. AddTappablefor inspection/side-effects mid-chain.Method Naming: Consistent verb prefixes across the entire package.
Prefix Contract Example hasReturns boolhasRole(),hasMedia()getReturns the value getMedia(),getTranslation()setAssigns the value setTranslation(),setLocale()addAppends addMedia(),addLogChange()findLooks up by identifier findByName(),findById()syncReplaces all syncRoles(),syncPermissions()clearRemoves all clearMediaCollection()using/withFluent configuration usingName(),withProperties()scopeQuery scope scopeRole(),scopePermission()Break these conventions and developers lose trust in the API.
Type-Rich Signatures: Accept everything reasonable. Return types on everything. Union types are a feature, not a smell.
IDE Coverage: Comprehensive
@throwson every method that can throw. Full@templateand@mixincoverage. Generics on builders,@mixinannotations on proxied classes. The IDE experience is mandatory.
The Anti-Patterns
| Don't | Do | Why |
|---|---|---|
| Two equally-valid entry points | One obvious path | Ambiguity erodes confidence |
| Builder methods that execute | Separate configuration from execution | Side effects in configuration are invisible bugs |
Return void from fluent methods |
Return $this or static |
Breaks method chaining |
| Missing return type annotations | Explicit types on every method | IDE and static analysis depend on it |
| Inconsistent verb prefixes | Follow the naming table above | Predictability is the API contract |
Accept only string when BackedEnum makes sense |
Union types: string or BackedEnum |
Meet developers where they are |
| Manual conditional logic in chains | Use Conditionable trait |
when() and unless() are cleaner |
See also: craft-trait (trait-based entry points), decide-facade (choosing the right entry point pattern).
Real-World Examples
See examples.md.