mcpskills.net
SkillsMCPsAgentsPrompts
mcpskills.net — A curated directory of AI agent Skills and MCP servers
TermsPrivacy
← Back to Skills
Development

API and Interface Design

Guides stable API and interface design. Use when designing APIs, module boundaries, or any public interface.

by Addy OsmaniRepository →Source →

Design stable, well-documented interfaces that are hard to misuse.

When to Use

  • Designing new API endpoints
  • Defining module boundaries or contracts between teams
  • Creating component prop interfaces
  • Establishing database schema that informs API shape
  • Changing existing public interfaces

Core Principles

Hyrum's Law

With a sufficient number of users of an API, all observable behaviors of your system will be depended on by somebody.

Every public behavior — including undocumented quirks — becomes a de facto contract once users depend on it.

The One-Version Rule

Design for a world where only one version exists at a time — extend rather than fork.

1. Contract First

Define the interface before implementing it:

interface TaskAPI {
  createTask(input: CreateTaskInput): Promise<Task>;
  listTasks(params: ListTasksParams): Promise<PaginatedResult<Task>>;
  getTask(id: string): Promise<Task>;
  updateTask(id: string, input: UpdateTaskInput): Promise<Task>;
  deleteTask(id: string): Promise<void>;
}

2. Consistent Error Semantics

interface APIError {
  error: {
    code: string;        // Machine-readable: "VALIDATION_ERROR"
    message: string;     // Human-readable: "Email is required"
    details?: unknown;
  };
}

// Status code mapping
// 400 → Client sent invalid data
// 401 → Not authenticated
// 403 → Authenticated but not authorized
// 404 → Resource not found
// 422 → Validation failed
// 500 → Server error (never expose internal details)

3. Validate at Boundaries

app.post('/api/tasks', async (req, res) => {
  const result = CreateTaskSchema.safeParse(req.body);
  if (!result.success) {
    return res.status(422).json({
      error: {
        code: 'VALIDATION_ERROR',
        message: 'Invalid task data',
        details: result.error.flatten(),
      },
    });
  }

  const task = await taskService.create(result.data);
  return res.status(201).json(task);
});

4. Prefer Addition Over Modification

// Good: Add optional fields
interface CreateTaskInput {
  title: string;
  description?: string;
  priority?: 'low' | 'medium' | 'high';  // Added later, optional
}

// Bad: Change existing field types
interface CreateTaskInput {
  title: string;
  priority: number;  // Changed from string — breaks consumers
}

5. Predictable Naming

| Pattern | Convention | Example | |---------|-----------|---------| | REST endpoints | Plural nouns, no verbs | GET /api/tasks | | Query params | camelCase | ?sortBy=createdAt | | Response fields | camelCase | { createdAt, updatedAt } | | Boolean fields | is/has/can prefix | isComplete | | Enum values | UPPER_SNAKE | "IN_PROGRESS" |

REST API Patterns

Resource Design

GET    /api/tasks              → List tasks
POST   /api/tasks              → Create a task
GET    /api/tasks/:id          → Get a single task
PATCH  /api/tasks/:id          → Update a task (partial)
DELETE /api/tasks/:id          → Delete a task

Pagination

// Request
GET /api/tasks?page=1&pageSize=20&sortBy=createdAt&sortOrder=desc

// Response
{
  "data": [...],
  "pagination": {
    "page": 1,
    "pageSize": 20,
    "totalItems": 142,
    "totalPages": 8
  }
}

Partial Updates (PATCH)

// Only title changes, everything else preserved
PATCH /api/tasks/123
{ "title": "Updated title" }

TypeScript Interface Patterns

Discriminated Unions for Variants

type TaskStatus =
  | { type: 'pending' }
  | { type: 'in_progress'; assignee: string; startedAt: Date }
  | { type: 'completed'; completedAt: Date; completedBy: string }
  | { type: 'cancelled'; reason: string; cancelledAt: Date };

function getStatusLabel(status: TaskStatus): string {
  switch (status.type) {
    case 'pending': return 'Pending';
    case 'in_progress': return `In progress (${status.assignee})`;
    case 'completed': return `Done on ${status.completedAt}`;
    case 'cancelled': return `Cancelled: ${status.reason}`;
  }
}

Branded Types for IDs

type TaskId = string & { readonly __brand: 'TaskId' };
type UserId = string & { readonly __brand: 'UserId' };

function getTask(id: TaskId): Promise<Task> { ... }

Verification

After designing an API:

  • [ ] Every endpoint has typed input and output schemas
  • [ ] Error responses follow a single consistent format
  • [ ] Validation happens at system boundaries only
  • [ ] List endpoints support pagination
  • [ ] New fields are additive and optional (backward compatible)
  • [ ] Naming follows consistent conventions across all endpoints