<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Api-Design on PHP Boy Scout</title><link>https://blog-570662.gitlab.io/tags/api-design/</link><description>Recent content in Api-Design on PHP Boy Scout</description><generator>Hugo -- gohugo.io</generator><language>en-gb</language><copyright>Matt Cockayne</copyright><lastBuildDate>Thu, 23 Apr 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://blog-570662.gitlab.io/tags/api-design/index.xml" rel="self" type="application/rss+xml"/><item><title>Two API decisions that quietly contradict each other</title><link>https://blog-570662.gitlab.io/two-api-decisions-that-quietly-contradict/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/two-api-decisions-that-quietly-contradict/</guid><description>&lt;img src="https://blog-570662.gitlab.io/two-api-decisions-that-quietly-contradict/cover-two-api-decisions-that-quietly-contradict.png" alt="Featured image of post Two API decisions that quietly contradict each other" /&gt;&lt;p&gt;Two design decisions on one enum, each sensible on its own, that would have quietly fought each other if I&amp;rsquo;d let them. I didn&amp;rsquo;t, but only because the second one is easy to get wrong and the compiler wouldn&amp;rsquo;t have said a word either way.&lt;/p&gt;
&lt;h2 id="decision-one-promise-the-list-can-grow"&gt;Decision one: promise the list can grow
&lt;/h2&gt;&lt;p&gt;&lt;a class="link" href="https://blog-570662.gitlab.io/just-enough-rust-to-follow-along/" &gt;&lt;code&gt;#[non_exhaustive]&lt;/code&gt;&lt;/a&gt; on the &lt;code&gt;Feature&lt;/code&gt; enum. It tells downstream code it can&amp;rsquo;t match the enum exhaustively, so it has to keep a wildcard arm, which in turn means adding a variant later is a non-breaking, minor-version change. Nobody&amp;rsquo;s &lt;code&gt;match&lt;/code&gt; stops compiling just because the enum grew. The doc comment says exactly that: it &amp;ldquo;keeps variant additions a minor-version change for downstream matchers.&amp;rdquo;&lt;/p&gt;
&lt;h2 id="decision-two-hand-out-the-whole-list"&gt;Decision two: hand out the whole list
&lt;/h2&gt;&lt;p&gt;A convenience &lt;code&gt;all()&lt;/code&gt; returning every variant, because iterating over the lot is something you genuinely want to do. The tempting signature is a fixed-size array, &lt;code&gt;[Feature; 11]&lt;/code&gt;: you know precisely how many there are, so why not put it in the type?&lt;/p&gt;
&lt;h2 id="why-those-two-cant-both-be-true"&gt;Why those two can&amp;rsquo;t both be true
&lt;/h2&gt;&lt;p&gt;The catch is a quirk of Rust that often trips up people arriving from other languages: the length of a fixed-size array is part of its &lt;em&gt;type&lt;/em&gt;. &lt;code&gt;[Feature; 11]&lt;/code&gt;, an array of exactly eleven features, and &lt;code&gt;[Feature; 12]&lt;/code&gt;, exactly twelve, are not one type holding a different number of items the way they might be elsewhere. They are two genuinely different, incompatible types, about as interchangeable as &lt;code&gt;i32&lt;/code&gt; and &lt;code&gt;i64&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;So the moment you add a twelfth variant, a fixed-size &lt;code&gt;all()&lt;/code&gt; forces an unhappy choice, and both options are bad. Bump the array to &lt;code&gt;[Feature; 12]&lt;/code&gt; and you break every caller who wrote the old length down. Leave it at &lt;code&gt;11&lt;/code&gt; and the new variant is silently dropped, leaving you a function called &lt;code&gt;all&lt;/code&gt; that doesn&amp;rsquo;t return all of them. Either way the &lt;code&gt;#[non_exhaustive]&lt;/code&gt; promise (adding a variant breaks nobody) is quietly cancelled by a return type that welded today&amp;rsquo;s count into the public API.&lt;/p&gt;
&lt;h2 id="so-all-returns-a-slice"&gt;So &lt;code&gt;all()&lt;/code&gt; returns a slice
&lt;/h2&gt;&lt;p&gt;Which is exactly what it does, and the doc comment spells out why, in &lt;a class="link" href="https://gitlab.com/phpboyscout/rust-tool-base/-/blob/9c22aa8/crates/rtb-app/src/features.rs#L59" target="_blank" rel="noopener"
 &gt;&lt;code&gt;crates/rtb-app/src/features.rs&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#[non_exhaustive]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="nc"&gt;Feature&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Init&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Docs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Mcp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Doctor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Ai&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Telemetry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Changelog&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Credentials&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-&amp;gt; &lt;span class="kp"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;&amp;#39;static&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="bp"&gt;Self&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="bp"&gt;Self&lt;/span&gt;::&lt;span class="n"&gt;Init&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;Self&lt;/span&gt;::&lt;span class="n"&gt;Version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;Self&lt;/span&gt;::&lt;span class="n"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cm"&gt;/* ...the rest... */&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A slice length is a value, not part of the type. Add a variant, the slice gets one longer, and not a single downstream signature changes. The promise holds.&lt;/p&gt;
&lt;h2 id="the-thing-to-watch-for"&gt;The thing to watch for
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;#[non_exhaustive]&lt;/code&gt; is a promise about the future. A fixed-size array is a fact about the present. You can&amp;rsquo;t keep both at once, and nothing will warn you that you&amp;rsquo;ve contradicted yourself, because each decision is individually fine. The trap is always the second API surface that quietly re-bakes the flexibility the first one promised. When you mark a type &amp;ldquo;free to grow,&amp;rdquo; go and check that nothing in its public interface has secretly written down how big it is today.&lt;/p&gt;</description></item><item><title>One variadic, and I'd already spent it</title><link>https://blog-570662.gitlab.io/one-variadic-and-id-already-spent-it/</link><pubDate>Thu, 26 Mar 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/one-variadic-and-id-already-spent-it/</guid><description>&lt;img src="https://blog-570662.gitlab.io/one-variadic-and-id-already-spent-it/cover-one-variadic-and-id-already-spent-it.png" alt="Featured image of post One variadic, and I'd already spent it" /&gt;&lt;p&gt;I had a constructor I was rather pleased with. Hand &lt;a class="link" href="https://blog-570662.gitlab.io/introducing-go-tool-base/" &gt;go-tool-base&lt;/a&gt;&amp;rsquo;s root command its props and as many sub-commands as you like, and off it goes. Then I needed to thread some config file paths through it, reached for the obvious &amp;ldquo;just add a parameter,&amp;rdquo; and discovered I&amp;rsquo;d already spent my one variadic with no second one going spare.&lt;/p&gt;
&lt;h2 id="the-ergonomics-id-happily-bought"&gt;The ergonomics I&amp;rsquo;d happily bought
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;NewCmdRoot&lt;/code&gt; ends in &lt;code&gt;subcommands ...*cobra.Command&lt;/code&gt;. That trailing &lt;code&gt;...&lt;/code&gt; is a small luxury: callers write &lt;code&gt;NewCmdRoot(props, build, deploy, status)&lt;/code&gt; and never have to think about slices. Variadics are lovely for exactly this, the &amp;ldquo;and as many of these as you fancy&amp;rdquo; argument.&lt;/p&gt;
&lt;h2 id="the-parameter-i-couldnt-add"&gt;The parameter I couldn&amp;rsquo;t add
&lt;/h2&gt;&lt;p&gt;Then config arrived, and the root command needed to know about some extra configuration file paths. The instinct is to add a parameter. The instinct is wrong, because there&amp;rsquo;s nowhere legal to put it.&lt;/p&gt;
&lt;p&gt;You can&amp;rsquo;t write &lt;code&gt;NewCmdRoot(props, configPaths ...string, subcommands ...*cobra.Command)&lt;/code&gt;. Go allows a function exactly one variadic, and it must be the final parameter. Two variadics results in a compile error before you&amp;rsquo;ve finished the line (assuming your IDE does compile time checks for you), and fairly so: at the call site, how would Go ever know where the strings stopped and the commands began? So the variadic I&amp;rsquo;d spent on sub-commands was spent. There wasn&amp;rsquo;t another to hand.&lt;/p&gt;
&lt;h2 id="the-choices-and-the-one-i-took"&gt;The choices, and the one I took
&lt;/h2&gt;&lt;p&gt;You can demote the variadic. Make it &lt;code&gt;subcommands []*cobra.Command&lt;/code&gt; and you&amp;rsquo;re free to add &lt;code&gt;configPaths []string&lt;/code&gt; next to it. Correct, and it breaks every existing call: &lt;code&gt;NewCmdRoot(props, build, deploy)&lt;/code&gt; becomes &lt;code&gt;NewCmdRoot(props, []string{}, []*cobra.Command{build, deploy})&lt;/code&gt;. Uglier at every site, to solve a problem only some callers have.&lt;/p&gt;
&lt;p&gt;You can reach for functional options, and for plenty of go-tool-base&amp;rsquo;s constructors that is exactly what happened. But the root builder is the one everybody calls first, with the simplest signature in the codebase, and I didn&amp;rsquo;t want the common case lugging option machinery around for the sake of the rare one.&lt;/p&gt;
&lt;p&gt;What I actually did was add a second door. From &lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/5c78fc9/pkg/cmd/root/root.go#L334-342" target="_blank" rel="noopener"
 &gt;&lt;code&gt;pkg/cmd/root/root.go&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// NewCmdRoot creates the root command with Props wiring and optional subcommands.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;NewCmdRoot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Props&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;subcommands&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;...*&lt;/span&gt;&lt;span class="nx"&gt;cobra&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;cobra&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Command&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;NewCmdRootWithConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;subcommands&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;NewCmdRootWithConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Props&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;configPaths&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;subcommands&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;...*&lt;/span&gt;&lt;span class="nx"&gt;cobra&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;cobra&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Command&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="c1"&gt;// ...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The new argument goes in as a plain &lt;code&gt;[]string&lt;/code&gt;, sat &lt;em&gt;before&lt;/em&gt; the variadic, which is perfectly legal: one variadic, still last. Callers who care about config use &lt;code&gt;NewCmdRootWithConfig&lt;/code&gt; explicitly, and &lt;code&gt;NewCmdRoot&lt;/code&gt; becomes a one-line wrapper that delegates with an empty slice, so every existing caller compiles untouched and none the wiser. Two doors into the same room, granted, but the original door is exactly where everyone left it.&lt;/p&gt;
&lt;h2 id="what-it-comes-down-to"&gt;What it comes down to
&lt;/h2&gt;&lt;p&gt;A trailing variadic is a slot you fill once. It buys gorgeous ergonomics for the &amp;ldquo;as many as you like&amp;rdquo; argument, and in exchange it quietly forecloses on ever appending another parameter, because the next one has nowhere to stand. Once it&amp;rsquo;s spent, new arguments come in as ordinary parameters before the variadic, and the kind thing to do for your callers is to put that behind a second constructor and let the original keep delegating.&lt;/p&gt;
&lt;p&gt;So spend the variadic deliberately. Give it to the argument that genuinely wants to be a loose list, not the first one that happens to be plural, because you don&amp;rsquo;t get a second.&lt;/p&gt;</description></item></channel></rss>