What Makes a Developer API Good?
Stipe's API is consistently cited as one of the best in the industry. Twilio, Clerk, and Resend get similar praise. What do they have in common?
It's not the features—it's the experience of using the features.
Principle 1: Predictability Over Cleverness
// Unpredictable: inconsistent response shapes
const user = await api.users.get(id); // returns { data: User }
const posts = await api.posts.list(); // returns User[] (no wrapper)
// Predictable: same shape everywhere
const user = await api.users.get(id); // returns User
const posts = await api.posts.list(); // returns Post[]
// OR always wrap:
const { data: user } = await api.users.get(id);
const { data: posts } = await api.posts.list();
Consistency is more important than elegance. Developers build mental models. Don't break them.
Principle 2: Errors That Tell You What to Do
// Bad error
throw new Error('Invalid input');
// Good error
throw new ApiError({
code: 'VALIDATION_ERROR',
message: 'The email field is required',
field: 'email',
docs: 'https://docs.example.com/api/users#create',
requestId: 'req_abc123', // for support
});
{
"error": {
"code": "STRIPE_CARD_DECLINED",
"message": "Your card was declined",
"decline_code": "insufficient_funds",
"doc_url": "https://stripe.com/docs/error-codes/card-declined",
"charge": "ch_abc123"
}
}
Stripe's errors tell you: what happened, why, what the user should do, and where to learn more. Copy this.
Principle 3: Resource-Oriented URLs
# Good: resources + HTTP verbs
GET /users → list users
POST /users → create user
GET /users/:id → get user
PATCH /users/:id → update user
DELETE /users/:id → delete user
GET /users/:id/posts → user's posts
# Bad: verb-in-URL (RPC style)
GET /getUser
POST /createUser
POST /deleteUser
POST /getUserPosts
REST conventions mean developers can guess your API shape before reading docs.
Principle 4: Sensible Defaults, Full Control
// All parameters optional — works with minimal input
const posts = await api.posts.list();
// returns 20 posts, sorted by createdAt desc, active only
// Full control when needed
const posts = await api.posts.list({
limit: 100,
offset: 200,
sort: 'title',
direction: 'asc',
status: 'draft',
userId: 'user_123',
});
Principle 5: Idempotency Keys
Networks fail. Clients retry. Without idempotency, retries create duplicates.
// Idempotency key prevents duplicate charges
await stripe.charges.create(
{ amount: 2000, currency: 'usd', source: 'tok_visa' },
{ idempotencyKey: `charge_${orderId}` } // safe to retry
);
For your own API:
app.post('/api/payments', async (req, res) => {
const idempotencyKey = req.headers['idempotency-key'];
if (idempotencyKey) {
const cached = await redis.get(`idempotency:${idempotencyKey}`);
if (cached) return res.json(JSON.parse(cached)); // return same response
}
const result = await processPayment(req.body);
if (idempotencyKey) {
await redis.setEx(`idempotency:${idempotencyKey}`, 86400, JSON.stringify(result));
}
res.json(result);
});
Principle 6: Pagination That Works
// Cursor-based (preferred for large datasets)
// GET /posts?limit=20&after=post_xyz
{
"data": [...],
"meta": {
"hasMore": true,
"nextCursor": "post_abc",
"total": null // omit for large tables
}
}
// Offset-based (simpler, but skip/take has performance issues at scale)
// GET /posts?page=5&limit=20
{
"data": [...],
"meta": {
"page": 5,
"limit": 20,
"total": 1847,
"pages": 93
}
}
Principle 7: Versioning From Day One
# URL versioning (simple, explicit)
https://api.example.com/v1/users
https://api.example.com/v2/users
# Header versioning (cleaner URLs)
API-Version: 2024-01-01
// Express version routing
const v1 = express.Router();
const v2 = express.Router();
v1.get('/users', v1UserHandler);
v2.get('/users', v2UserHandler); // different response shape
app.use('/v1', v1);
app.use('/v2', v2);
TypeScript SDK Generation
Document your API with OpenAPI, generate typed clients:
# Generate TypeScript client from OpenAPI spec
npx openapi-typescript-codegen \
--input https://api.example.com/openapi.json \
--output ./src/api-client
Now your API users get autocomplete and type checking for free.
The best DX is one where developers succeed without reading docs. Work backwards from that goal.
RESTful API patterns, versioning, error handling, and OpenAPI docs: Whoff Agents AI SaaS Starter Kit.
Top comments (0)