7 years ago I published my first library to npm custom-border-mixin. Back then, I disappeared for a week writing an SCSS mixin for fun. If you wonder what fun might be in SCSS, just look at the helper:
@function param-get($parameters, $key) {
$value: map-get($parameters, $key);
@if $key == 'side' and $value != 'top' and $value != 'right' and $value != 'bottom' and $value != 'left' {
@error 'Value #{$value} of property #{$key} must be either top, or right, or bottom, or left, or vertical, or horizontal, or all.';
}
@if ($key == 'size' or $key == 'length' or $key == 'gap') and (type-of($value) != number or type-of($value) == number and $value < 0) {
@error 'Value #{$value} of property #{$key} must be non-negative size number.';
}
@if $key == 'color' and type-of($value) != color {
@error 'Value #{$value} of property #{$key} must be color.';
}
@if $key == 'start' and $value != 'origin' and $value != 'center' and $value != 'opposite' {
@error 'Value #{$value} of property #{$key} must be either origin, center, or opposite.';
}
@return $value;
}
Enter fullscreen mode Exit fullscreen mode
Nobody wanted, and nobody asked for, the mixin, but I think it sparked some passion inside of me. This is probably how many people come to open source.
My name is Dmitry, and in this article I’ll share how my vision for library design and public APIs was shaped by the rise of agentic programming. And how it actually didn’t change what a good library is.
I worked on a platform team, then 5 years ago created my personal open-source project Sury. It’s v11 and still going. Currently, I work at Envio where I shape and build the fastest blockchain indexing tool (HyperIndex GitHub).
How has AI changed library API design?
In an ideal world, there should be little to no difference between an API for a human being and an AI agent. It just happened that, when designing libraries before, we often relied on the expectation that users read the docs, have prior knowledge, or context. With agents, this doesn’t always work, so it’s important to design the library so the API drives users…
…To fall into the pit of success – I know, I know, but it’s never old.
Besides obvious ones like guiding error messages, here are some practices I used when shipping Sury v11 release. Sury is a JavaScript schema library, and I’ll use it for examples from now on.
1. Prefer explicit over implicit
There’s no parse in Sury. I didn’t want to give a default that throws somewhere down the line, and nobody handles it. So there are two names, and you have to choose:
S.parseOrThrow(userSchema, data);
// { id: "p_1" }
S.parseAsResult(userSchema, data);
// { success: true, value: { id: "p_1" } }
// { success: false, error: SuryError: Expected string, received 42 }
Enter fullscreen mode Exit fullscreen mode
Now the choice is written down. The agent is more likely to take parseAsResult, and on review another agent can actually see that nobody handles the error.
Compare it with Zod, where the throwing one gets the short name:
userSchema.parse(data); // throws
userSchema.safeParse(data); // returns a result
Enter fullscreen mode Exit fullscreen mode
Without the safeParse line, parse would look similarly safe, doesn’t it? Nothing in parse tells you that an exception is coming, and the safe version is hidden behind a name you have to know about.
Ironically, it matters even more now that an AI does the review. A human at least could have a bad feeling about parse.
The same story, but worse
Now look at is/validate. Every schema library has something like it, and at first glance it looks completely harmless:
if (is(userSchema, data)) {
// data is a User, right?
}
Enter fullscreen mode Exit fullscreen mode
But if the schema transforms something, there are two answers to this question. Every library picks one for you, and, surprise, they don’t pick the same one:
The helper What it checks Zodz.validate(schema, data)
Input
Valibot
v.is(schema, data)
Input
ArkType
schema.allows(data)
Input
TypeBox
Value.Check(schema, data)
Input
io-ts
codec.is(data)
Output
Effect
Schema.is(schema)(data)
Output
Superstruct
is(data, struct)
Output
Yup
schema.isValidSync(data)
converts first, so both pass
Joi
schema.validate(data)
converts first, so both pass
Sury
S.isInput / S.isOutput
the one you picked
Same call, opposite meaning, depending on what’s in your package.json. And Yup with Joi convert the value first, so they just say yes to both.
Russian roulette in disguise. 👀
That’s why there’s no S.is:
const priceSchema = S.string.with(S.to, S.number);
S.isInput(priceSchema, "42"); // true
S.isOutput(priceSchema, "42"); // false
Enter fullscreen mode Exit fullscreen mode
2. Any way where it doesn’t matter
An agent that guessed the argument order wrong spends an extra iteration on it. The same goes for humans, but at least they’ll probably remember it after a second time. But why should they? So I made every order work:
S.parseOrThrow(data, userSchema);
S.parseOrThrow(userSchema, data);
S.parseOrThrow(userSchema)(data);
S.isInput(data, userSchema);
S.isInput(userSchema, data);
S.isInput(userSchema)(data);
Enter fullscreen mode Exit fullscreen mode
All of them give the same result, so any guess is the right guess.
3. Force a decision where it matters
When a case can be read in more than one way, I don’t want to pick a default for you. I make you choose:
S.decodeOrThrow(S.env, S.string);
// SuryError: Ambiguous "" for string. Should a blank input be rejected,
// kept, or read as absent? Choose with S.nonEmpty, S.minLength(0),
// or S.optional
Enter fullscreen mode Exit fullscreen mode
And it throws when you build the decoder, not when the data arrives, so you see it while writing the code. An empty env var is a decision, and I don’t think my library should make it for you.
4. Aliases for common knowledge
As much as I’d like to force my own API, the agent/human-being first tries the syntax from their own knowledge. Fighting them costs an iteration and frustration, so I just made aliases:
S.union([S.literal("admin"), S.object({ role: S.literal("user") })]);
S.union([S.schema("admin"), S.schema({ role: S.schema("user") })]);
S.union(["admin", { role: "user" }]);
// all three: Schema<"admin" | { role: "user" }>
Enter fullscreen mode Exit fullscreen mode
And if you don’t like one of the spellings in your own codebase, that’s a linter rule, not a decision I should make for everybody:
// eslint.config.js
"no-restricted-syntax": ["error", {
selector: "CallExpression[callee.object.name='S'][callee.property.name='object']",
message: "Use S.schema instead of S.object",
}]
Enter fullscreen mode Exit fullscreen mode
Back to the pit of success
Honestly, none of this is really about AI. A library that drives its own usage was always better for people too. It’s just that agents stopped forgiving the parts we used to cover with docs.
All of this is in Sury v11, which is out now. And if you want more about schema libraries and library design, follow me on X – it’ll make my day 🙏