Designing APIs Developers Actually Enjoy
What makes an API feel right — and how to build one that developers reach for without hesitation.
Great APIs don't announce themselves. They simply work the way you expect them to.
Start with the call site
Before writing any implementation, write the code you wish existed:
const env = createEnv({
server: { DATABASE_URL: z.string().url() },
client: { NEXT_PUBLIC_APP_URL: z.url() },
});If the call site feels awkward, the API is wrong — not the user.
Defaults are a feature
Every optional parameter is a decision you're making on behalf of the developer. Good defaults mean the common case requires zero configuration.
- Sensible timeouts — don't make users guess
- Clear error messages — tell them exactly what's wrong and how to fix it
- Type inference — let TypeScript do the heavy lifting
Errors should teach
When something goes wrong, the error message is your best documentation:
Missing required environment variable: DATABASE_URL
Expected: string (URL format)
Received: undefined
Add DATABASE_URL to your .env file.
Compare that to Error: Invalid config. One teaches, one frustrates.
Composition over configuration
Prefer small, composable functions over monolithic configs. Developers should be able to use one piece without importing the whole library.
The invisible details
The best APIs feel invisible because every edge case has been considered:
- What happens on the second call?
- What if the input is empty?
- Does it work in SSR?
These questions, answered well, are what separate good libraries from great ones.