|
| 1 | +# FastAPI / OpenAPI Standards |
| 2 | + |
| 3 | +Our OpenAPI spec drives our SDK, Scalar docs, and agent tool use (Kiln Chat calls our APIs). Every endpoint must be well-documented and consistently named. Flag violations during code review. |
| 4 | + |
| 5 | +**Required on every endpoint:** |
| 6 | + |
| 7 | +1. **`tags=[...]`** on the route decorator. Every endpoint must belong to a tag group (e.g. `tags=["Projects"]`). Untagged endpoints break Scalar navigation and agent tool discovery. Prefer existing tags, creating new ones only when really needed. All tags should be documented in `tags_metadata` in `server.py` |
| 8 | +2. **`summary=`** on the route decorator. A short, unique name for the operation. Summaries must be unambiguous — if two endpoints could share the same summary (e.g. "Edit Tags"), qualify them ("Edit Run Tags", "Edit Document Tags"). |
| 9 | +3. **Docstring** on the handler function (optional if behavior is completely obvious from the path, method, and summary). When provided, docstrings should be terse — one sentence or a fragment. Never pad with filler like "This endpoint allows you to...". Longer descriptions (2–3 sentences) are warranted only when distinguishing easily confused endpoints, documenting non-obvious side effects, or noting prerequisites. Exclude if the `summary` string already covers the same level of detail. |
| 10 | +4. **`Path(description=...)`** on every path parameter, using `Annotated[str, Path(description="...")]` syntax. Recurring ID parameters must use consistent standard descriptions (e.g. `"The unique identifier of the project."`, `"The unique identifier of the task within the project."`). |
| 11 | +5. **`Query(description=...)`** on every query parameter. |
| 12 | +6. **`Field(description=...)`** on Pydantic model properties that aren't completely self-evident from name + type. |
| 13 | +7. **Class docstring** on Pydantic models used as API request/response bodies. These become the schema description in the OpenAPI spec, which agents and SDK users see when inspecting request/response types. Optional but suggested if non-obvious from name. |
| 14 | + |
| 15 | +**Correct HTTP methods:** |
| 16 | + |
| 17 | +- **GET** must be idempotent and side-effect-free. If an endpoint creates, modifies, or deletes data, it must not be GET. We previously had GET endpoints that established connections and ran evaluations — this is wrong and confuses both agents and humans. |
| 18 | +- **POST** for creation and actions that trigger execution. |
| 19 | +- **PATCH** for partial updates. |
| 20 | +- **DELETE** for deletion. |
| 21 | +- The only exception is SSE streaming endpoints, which must use GET due to browser `EventSource` constraints. These must have descriptions explicitly noting the mutation and the SSE reason. |
| 22 | + |
| 23 | +**Naming and path conventions:** |
| 24 | + |
| 25 | +- **Always use plural nouns** in path segments: `/tasks/{task_id}`, never `/task/{task_id}`. Same for `/projects`, `/specs`, `/evals`, `/runs`, `/prompts`, `/documents`, `/skills`, `/run_configs`, etc. We had inconsistencies where GET used plural but POST/PATCH/DELETE used singular — this is confusing and must be caught. |
| 26 | +- **Paths should be descriptive and intuitive.** Paths should follow REST conventions and be clear (as possible) without docstrings. Path and descriptions should distinguishing similar sounding endpoints. If a path could reasonably be improved, suggest a rename. |
| 27 | +- **Consistent path structure** for related resources. All operations on the same resource type should share a common path prefix (e.g. all run config operations under `/run_configs`, not split across `/task_run_config`, `/mcp_run_config`, `/run_config`). Important to not use similar but different prefixes, as this commonly trips up agents. |
| 28 | +- **No trailing slashes** on paths. Use `/run_configs` not `/run_configs/`. Trailing slashes cause inconsistency between endpoints and can break client routing. |
| 29 | + |
| 30 | +**Example of a well-documented endpoint:** |
| 31 | + |
| 32 | +```python |
| 33 | +@app.delete( |
| 34 | + "/api/projects/{project_id}", |
| 35 | + summary="Delete Project", |
| 36 | + tags=["Projects"], |
| 37 | +) |
| 38 | +async def delete_project( |
| 39 | + project_id: Annotated[ |
| 40 | + str, Path(description="The unique identifier of the project.") |
| 41 | + ], |
| 42 | +) -> dict: |
| 43 | + """Removes the project from Kiln but does not delete the files from disk.""" |
| 44 | +``` |
| 45 | + |
| 46 | +**What to flag in code review:** |
| 47 | + |
| 48 | +- Missing `tags=` on any route decorator |
| 49 | +- Missing `summary=` on any route decorator |
| 50 | +- Missing `Path(description=...)` or `Query(description=...)` on any parameter |
| 51 | +- GET endpoints that perform mutations (unless SSE with documented justification) |
| 52 | +- Singular nouns in path segments where plural is standard |
| 53 | +- Ambiguous or duplicate summaries across endpoints |
| 54 | +- Trailing slashes on paths |
| 55 | +- Inconsistent path naming for the same resource type |
| 56 | +- Wordy or filler-padded docstrings ("This endpoint allows you to...") |
| 57 | +- Docstrings containing code artifacts, raw `Args:` blocks, or formatting that doesn't read as clean prose in OpenAPI |
| 58 | +- Pydantic models used in API request/response types (nested included) missing a class docstring, if the class name alone isn't obvious |
| 59 | +- Custom string types with validator-based constraints that don't surface in the OpenAPI schema. Use `StringConstraints` in the `Annotated` type definition so `minLength`/`maxLength` appear automatically (see `FilenameString`, `SkillNameString` for examples). Don't duplicate constraints in individual `Field()` calls. |
0 commit comments