Skip to content

$ less ~/blog/api-design-principles.md

API Design Principles: Building APIs Developers Love

Why Iris uses OpenAI's API spec and what I learned about API design building local-first tools. Compatibility beats purity every time.

When I built Iris, I had a choice: design a perfect, clean API from scratch, or copy OpenAI's format exactly. I copied. Here's why that was the right call and what it taught me about API design.

The OpenAI-Compatible Decision

Iris is a local AI assistant. Runs entirely on your machine. No cloud. No API keys. But it exposes this:

POST /v1/chat/completions
Authorization: Bearer local-only

{
  "model": "iris",
  "messages": [
    {"role": "user", "content": "What's the weather?"}
  ]
}

That's OpenAI's API. Not "inspired by" or "similar to"—it's the exact same format. Point your OpenAI SDK at localhost:8080 and it works. No changes. No adapter layer.

Why? Because every tool already supports it. VSCode extensions, CLI tools, web apps—anything built for OpenAI works with Iris immediately. That's not elegance, that's leverage.

What Actually Matters in API Design

Forget REST purity. Here's what makes APIs good or bad in practice:

1. Don't Make Me Read Docs

Good APIs are predictable. If you've seen one endpoint, you've seen them all. Sysmon-CLI's metrics API follows one pattern:

GET /metrics/cpu
GET /metrics/memory
GET /metrics/disk
GET /metrics/network

Same response shape. Same query params. Same error format. You learn it once.

Bad APIs surprise you. Different endpoints need different auth headers. Error formats change. Status codes are creative interpretations of HTTP.

2. Compatibility > Clever Design

The OpenAI API isn't perfect. The messages array gets repetitive. The model field is redundant when you're hitting localhost. But changing it means every integration needs custom code.

I wanted to add a context_window param directly in the request. Cleaner. More obvious. But then I'd need to maintain SDK forks, browser extensions, and CLI wrappers. Not worth it.

Boring and compatible wins. Every time.

3. Errors Should Tell You What to Fix

Here's a bad error:

{"error": "Invalid request"}

Here's what Zvezda returns when you mess up a Git remote URL:

{
  "error": {
    "code": "invalid_remote",
    "message": "Git remote URL must start with https:// or git@",
    "details": {
      "provided": "github.com/user/repo",
      "examples": [
        "https://github.com/user/repo.git",
        "git@github.com:user/repo.git"
      ]
    }
  }
}

You know exactly what's wrong. You know how to fix it. No Googling. No guessing.

4. Status Codes: Use the Boring Ones

You need maybe 8 status codes:

  • 200: It worked
  • 201: Created something
  • 400: You sent bad data
  • 401: Missing/invalid auth
  • 404: Doesn't exist
  • 429: Slow down
  • 500: I broke something
  • 503: Temporarily down

That's it. Don't get creative. I've seen APIs return 418 (I'm a teapot) for rate limiting. Very funny. Also useless—most HTTP clients don't handle it.

Real Examples from My Tools

Iris: When to Ignore REST

Iris has a /v1/models endpoint that returns... one model. Always. It exists because OpenAI clients expect it, not because it's useful.

GET /v1/models
{
  "data": [
    {"id": "iris", "object": "model"}
  ]
}

Is this wasteful? Yes. Does it matter? No. Compatibility is the feature.

Sysmon-CLI: Keep It Simple

Sysmon-CLI exposes metrics over HTTP. I could've built a GraphQL API where you request exactly the fields you want. Or a sophisticated query language. Or subscriptions with SSE.

Instead:

GET /metrics/cpu?interval=1s
{
  "timestamp": "2025-01-09T10:30:00Z",
  "usage_percent": 23.4,
  "cores": [12.1, 34.2, 18.9, 29.3]
}

JSON over HTTP. That's it. Works with curl. Works with fetch(). Works with every language's stdlib. No dependencies. No learning curve.

Zvezda: Local-Only Auth

Zvezda manages Git repos. Runs on localhost. Needs auth so random scripts can't mess with your repos. But it's just you.

Authorization: Bearer zvezda-local-token

That token? It's in ~/.config/zvezda/token. Generated once. Never expires. Not JWT. Not OAuth. Just a random string that proves the request came from your machine.

Overengineering would be: OAuth2 flows, token refresh, PKCE, scopes, the works. For an API that only accepts connections from localhost.

What I Avoid

Custom HTTP verbs. I've seen LINK, UNLINK, MERGE. Use POST. Everyone understands POST.

Nested REST resources beyond 2 levels. /users/123/repos/456/issues/789/comments/012 is unreadable. Flatten it: /comments/012.

Required versioning from day 1. Iris started at /v1 because OpenAI does. Sysmon-CLI doesn't version—it's not a public API, I can change it. Zvezda has no version. If I need breaking changes, I'll add /v2 then.

Pagination for small datasets. Zvezda's repo list never exceeds 1000 entries. No pagination. Just return the array. When it becomes a problem, I'll fix it. YAGNI applies to APIs too.

The One Rule

Make it work with `curl` first.

If your API needs a custom client to be usable, you're doing it wrong. Every endpoint should have a curl example that just works.

# Good
curl http://localhost:8080/metrics/cpu

# Bad (needs custom client because WebSocket handshake)
wscat -c ws://localhost:8080/metrics

REST is boring. JSON is boring. Bearer tokens are boring. Boring is good. Boring means it works everywhere with zero setup.

When to Ignore All This

If you're building for scale—billions of requests, thousands of services, multi-region deployments—ignore everything I said. Use gRPC. Use Protobuf. Version everything. Design for backwards compatibility from day 1.

But if you're building tools for developers? Local-first apps? CLI utilities? Keep it simple. Copy what works. Compatibility beats purity.

Iris didn't need to reinvent chat APIs. Sysmon-CLI didn't need GraphQL. Zvezda didn't need OAuth2.

They needed to work. With curl. Right now. That's the entire design spec.

(END) — press q… or just go back to ~/blog

more in technical/

Independent studio building developer tools, local-first software and custom projects.

contact@nf-software.com
© 2026 NF Software · SIRET 104 780 820 00015privacyterms
NORMAL~/blog/api-design-principlesTop