← Engineering Notes

API Design

API and Service Design · last updated Oct 2026

Concept

An API is a contract. It defines what a service accepts, what it returns, and what errors it produces. Good API design means that contract is consistent, predictable, and explicit — so that clients can rely on it without reading the implementation.

This note focuses on HTTP REST APIs, which is the style I use in Simple Bank, GoTP, and the URL Shortener.

Problem

A poorly designed API creates friction for everyone who uses it. Error responses that are inconsistent or uninformative make debugging difficult. Endpoints that behave differently in edge cases are unpredictable. An API that leaks implementation details couples clients to internals they shouldn't know about.

The cost of a bad API design compounds over time. If clients depend on inconsistent behaviour, you can't fix it without breaking them.

How it works

A few principles I find useful:

Use HTTP semantics correctly. GET requests should not have side effects. POST creates resources. PUT replaces. PATCH modifies partially. DELETE removes. Using these correctly gives clients and infrastructure predictable behaviour — caches know GET responses can be cached, for example.

Be consistent with resource naming. Plural nouns for collections (/accounts), singular identifiers for specific resources (/accounts/123). Avoid verbs in endpoints — the HTTP method already expresses the action.

Use HTTP status codes accurately. 200 for success, 201 for created, 400 for invalid input, 401 for unauthenticated, 403 for unauthorized, 404 for not found, 409 for conflicts, 500 for unexpected server errors. Clients use these codes to handle responses programmatically.

Return consistent error responses. Every error should have the same shape, so clients have one code path for handling them. A useful structure:

{
  "error": "account_not_found",
  "message": "No account with ID 123 exists."
}

Validate at the boundary. Validate all input as early as possible, before it enters the system. Return a 400 with a clear message describing what is wrong. Don't let invalid data propagate inward.

Trade-offs

Strict REST semantics can become awkward for operations that don't map cleanly to CRUD. A bank transfer isn't simply creating a resource — it's an operation with side effects on multiple resources. Some designs use action-oriented endpoints (e.g., POST /accounts/123/transfer) when a pure resource model doesn't fit.

Being too permissive with input makes validation harder. Being too strict makes the API fragile. The balance is to validate what matters for correctness and security, and be lenient about things that don't.

Practical application

In Simple Bank, every handler validates input using the gin binding tags, which map directly to request validation. Errors from validation produce a 400 response with a structured error body. The handler only proceeds if validation passes.

In GoTP, the API must be careful about what it exposes in error responses. Telling a caller "that phone number doesn't exist in our system" leaks information. Instead, error messages are intentionally generic for authentication and OTP endpoints.

My understanding

The most important thing I've learned about API design is that consistency matters more than cleverness. An API that behaves the same way in every case — same error format, same status code conventions, same naming patterns — is far easier to work with than one that has "better" behaviour in some cases but surprises you in others.

Design the API from the consumer's perspective first. What does the caller need? What information do they need in an error? What should be transparent and what should be an implementation detail?

← Back to Engineering Notes