|
| 1 | +--- |
| 2 | +name: flow-code-api-design |
| 3 | +description: "Use when designing or modifying APIs, module boundaries, CLI interfaces, library public surfaces, RPC contracts, or event schemas" |
| 4 | +--- |
| 5 | + |
| 6 | +# API and Interface Design |
| 7 | + |
| 8 | +## Overview |
| 9 | + |
| 10 | +Contract-first interface design: define the interface before implementing it, then enforce it at boundaries. Applicable to REST APIs, CLI tools, library APIs, RPC services, event schemas, and module boundaries. Good interfaces make the right thing easy and the wrong thing hard. |
| 11 | + |
| 12 | +## When to Use |
| 13 | + |
| 14 | +- Designing a new API, CLI command, library public surface, or RPC contract |
| 15 | +- Changing an existing public interface (adding fields, modifying behavior) |
| 16 | +- Defining cross-team or cross-module contracts |
| 17 | +- Establishing event schemas or message formats |
| 18 | +- Creating module boundaries within a codebase |
| 19 | + |
| 20 | +**When NOT to use:** |
| 21 | +- Internal implementation details behind a stable interface |
| 22 | +- Private functions within a module |
| 23 | +- Refactoring internals without changing the public surface |
| 24 | +- If debugging an API issue, use the `flow-code-debug` skill instead |
| 25 | + |
| 26 | +## Core Process |
| 27 | + |
| 28 | +### Phase 1: Define the Contract First |
| 29 | + |
| 30 | +Design the interface before writing any implementation. The contract is the spec. |
| 31 | + |
| 32 | +1. **Identify consumers** -- who calls this interface? Other services, CLI users, library consumers, event subscribers? |
| 33 | +2. **Define input/output shapes** -- what goes in, what comes out. Be explicit about types, optionality, and defaults. |
| 34 | +3. **Specify error semantics** -- every interface has failure modes. Define them upfront. |
| 35 | + |
| 36 | +``` |
| 37 | +Example contract (stack-agnostic pseudocode): |
| 38 | +
|
| 39 | + interface TaskStore: |
| 40 | + create(input: CreateTaskInput) -> Task | ValidationError |
| 41 | + get(id: string) -> Task | NotFoundError |
| 42 | + list(filter: Filter, page: Page) -> PaginatedResult<Task> |
| 43 | + update(id: string, patch: Partial<Task>) -> Task | NotFoundError | ValidationError |
| 44 | + delete(id: string) -> void // idempotent: succeeds even if already deleted |
| 45 | +``` |
| 46 | + |
| 47 | +For CLI tools, the contract is the command signature: |
| 48 | +```bash |
| 49 | +# Contract: flowctl task create --title <str> [--domain <str>] [--files <paths>] |
| 50 | +# Output: JSON with { id, title, status } on success |
| 51 | +# Exit code: 0 success, 1 validation error, 2 not found |
| 52 | +``` |
| 53 | + |
| 54 | +For event schemas: |
| 55 | +``` |
| 56 | +Event: task.completed |
| 57 | + payload: { task_id: string, completed_at: timestamp, duration_seconds: int } |
| 58 | + guarantees: at-least-once delivery, idempotent consumers expected |
| 59 | +``` |
| 60 | + |
| 61 | +### Phase 2: Apply Hyrum's Law Awareness |
| 62 | + |
| 63 | +> With a sufficient number of users, all observable behaviors become de facto contracts. |
| 64 | +
|
| 65 | +Every public behavior -- including undocumented quirks, error message text, ordering, and timing -- becomes a commitment once consumers depend on it. |
| 66 | + |
| 67 | +Design implications: |
| 68 | +- Be intentional about what you expose. If consumers can observe it, they will depend on it. |
| 69 | +- Do not leak implementation details through the interface (internal IDs, database structure, stack traces). |
| 70 | +- Treat error message formats as part of the contract -- consumers parse them. |
| 71 | +- For CLI tools: exit codes, output format (JSON vs text), and flag names are all contract surface. |
| 72 | + |
| 73 | +### Phase 3: Design Error Semantics |
| 74 | + |
| 75 | +Pick one error strategy and use it consistently across the entire interface: |
| 76 | + |
| 77 | +**Structured errors (recommended):** |
| 78 | +``` |
| 79 | +Error shape (any transport): |
| 80 | + code: string // Machine-readable: "VALIDATION_ERROR", "NOT_FOUND" |
| 81 | + message: string // Human-readable: "Title is required" |
| 82 | + details?: any // Additional context when helpful |
| 83 | +
|
| 84 | +Transport mapping: |
| 85 | + REST: code -> HTTP status (400, 404, 409, 422, 500) |
| 86 | + CLI: code -> exit code + JSON stderr |
| 87 | + RPC: code -> status enum + metadata |
| 88 | + Event: code -> dead-letter reason |
| 89 | +``` |
| 90 | + |
| 91 | +Do not mix patterns. If some operations throw, others return null, and others return `{ error }` -- consumers cannot predict behavior. |
| 92 | + |
| 93 | +### Phase 4: Validate at Boundaries |
| 94 | + |
| 95 | +Trust internal code. Validate where external input enters the system: |
| 96 | + |
| 97 | +**Where validation belongs:** |
| 98 | +- API route handlers (user input) |
| 99 | +- CLI argument parsing (user input) |
| 100 | +- External service response parsing (third-party data -- always untrusted) |
| 101 | +- Event/message deserialization (cross-service data) |
| 102 | +- Environment variable loading (configuration) |
| 103 | + |
| 104 | +**Where validation does NOT belong:** |
| 105 | +- Between internal functions that share type contracts |
| 106 | +- In utility functions called by already-validated code |
| 107 | +- On data that just came from your own database |
| 108 | + |
| 109 | +### Phase 5: Plan for Evolution |
| 110 | + |
| 111 | +Design interfaces that can grow without breaking consumers: |
| 112 | + |
| 113 | +1. **Prefer addition over modification** -- new optional fields, new endpoints/commands, new event types. |
| 114 | +2. **Never remove or change existing fields** -- every consumer becomes a constraint. |
| 115 | +3. **Version when breaking changes are unavoidable** -- but prefer extension first. |
| 116 | +4. **Use the One-Version Rule** -- avoid forcing consumers to choose between multiple versions. |
| 117 | + |
| 118 | +For CLI tools: new flags default to off, new subcommands are additive, output format changes require `--format` flags. |
| 119 | + |
| 120 | +### Phase 6: Design for Idempotency |
| 121 | + |
| 122 | +Idempotency applies beyond HTTP -- CLI operations, event handlers, and RPC calls all benefit: |
| 123 | + |
| 124 | +- **DELETE/remove operations**: succeed even if resource already gone. |
| 125 | +- **CLI operations**: `flowctl lock --task T1 --files f.py` is safe to call twice. |
| 126 | +- **Event handlers**: processing the same event twice produces the same result. |
| 127 | +- **Create with client ID**: client provides an idempotency key; server deduplicates. |
| 128 | + |
| 129 | +``` |
| 130 | +Idempotency decision: |
| 131 | + Is the operation naturally idempotent (GET, PUT, DELETE)? -> No extra work. |
| 132 | + Is it a create/mutation? -> Require idempotency key or make it upsert. |
| 133 | + Is it an event handler? -> Track processed event IDs or make handler pure. |
| 134 | +``` |
| 135 | + |
| 136 | +### Phase 7: Document the Contract |
| 137 | + |
| 138 | +The contract must be committed alongside the implementation: |
| 139 | + |
| 140 | +- Type definitions or schema files (protobuf, JSON Schema, OpenAPI, CLI --help) |
| 141 | +- Error code catalog (all possible error codes with descriptions) |
| 142 | +- Examples of success and failure responses |
| 143 | +- Migration notes for any changes to existing interfaces |
| 144 | + |
| 145 | +Reference `references/code-review-checklist.md` for review patterns -- particularly the Architecture and Correctness sections when reviewing API changes. |
| 146 | + |
| 147 | +## Common Rationalizations |
| 148 | + |
| 149 | +| Excuse | Reality | |
| 150 | +|--------|---------| |
| 151 | +| "I'll design the API after implementation" | API shapes emerge from implementation but don't match consumer needs. Contract-first catches mismatches before code exists. | |
| 152 | +| "Internal APIs don't need contracts" | Hyrum's Law applies to internal APIs too. Internal consumers are still consumers -- contracts prevent coupling and enable parallel work. | |
| 153 | +| "We can always change it later" | Every consumer becomes a constraint. The cost of change grows exponentially with adoption. | |
| 154 | +| "The code is the documentation" | Consumers shouldn't need to read implementation to use your API. The contract is the interface, not the code behind it. | |
| 155 | +| "Edge cases can wait" | Edge cases in APIs become permanent undefined behavior. Consumers will discover them and depend on whatever happens. | |
| 156 | +| "We don't need error codes yet" | Without structured errors from day one, consumers parse message strings. Changing error format later breaks every consumer. | |
| 157 | +| "This CLI is just for internal use" | Internal CLI tools accumulate scripts that depend on exact output format. Same contract discipline applies. | |
| 158 | +| "Versioning is overkill" | Breaking changes without versioning break consumers silently. Design for extension from the start. | |
| 159 | + |
| 160 | +## Red Flags |
| 161 | + |
| 162 | +- Interface returns different shapes depending on conditions (inconsistent contract) |
| 163 | +- Error formats differ across operations in the same API |
| 164 | +- Validation scattered throughout internal code instead of at boundaries |
| 165 | +- Breaking changes to existing fields (type changes, removals, renamed flags) |
| 166 | +- List/search operations without pagination or size limits |
| 167 | +- CLI commands with unparseable free-text output instead of structured (JSON) output |
| 168 | +- No idempotency story for mutating operations |
| 169 | +- Implementation details leaking through the interface (database IDs, internal error traces) |
| 170 | +- Contract defined only in code comments, not in types or schema files |
| 171 | + |
| 172 | +## Verification |
| 173 | + |
| 174 | +After designing or modifying an interface: |
| 175 | + |
| 176 | +- [ ] Contract defined before implementation (types, schema, or CLI signature committed first) |
| 177 | +- [ ] Every operation has explicit input and output shapes with typed errors |
| 178 | +- [ ] Error responses follow a single consistent format across all operations |
| 179 | +- [ ] Validation happens at system boundaries only (not scattered internally) |
| 180 | +- [ ] New fields/flags are additive and optional (backward compatible) |
| 181 | +- [ ] Mutating operations have an idempotency strategy |
| 182 | +- [ ] Contract documentation committed alongside implementation |
| 183 | +- [ ] Reviewed against `references/code-review-checklist.md` Architecture and Correctness sections |
0 commit comments