# Add an API Endpoint **When to use:** You need to add a new HTTP endpoint to the Episteme API. ## Prerequisites - Familiarity with `axum` handler patterns - Understanding of `utoipa` derive macros - Read [ai-lookup/services/api.md](../../../ai-lookup/services/api.md) for architecture overview ## Quick Start ```bash # After adding endpoint code: cargo build -p stemedb-api cargo test -p stemedb-api cargo clippy -p stemedb-api -- -D warnings ``` ## Step-by-Step ### 1. Define DTO Types Create request/response types in `crates/stemedb-api/src/dto.rs`. These derive BOTH serde (for JSON) and utoipa (for OpenAPI docs): ```rust #[derive(Serialize, Deserialize, ToSchema)] pub struct MyRequest { /// Description appears in OpenAPI docs. #[schema(example = "Tesla_Inc")] pub subject: String, } #[derive(Serialize, Deserialize, ToSchema)] pub struct MyResponse { pub hash: String, pub status: String, } ``` Rules: - DTOs live in `dto.rs`, NOT in handler files - Implement `From` for conversions to/from internal types - Use `#[schema(example = "...")]` for meaningful OpenAPI examples - Never expose internal rkyv types in the public API ### 2. Write the Handler Create handler in `crates/stemedb-api/src/handlers/{name}.rs`: ```rust #[utoipa::path( post, path = "/my-endpoint", request_body = MyRequest, responses( (status = 202, description = "Accepted", body = MyResponse), (status = 400, description = "Validation failed", body = ApiError), ), tag = "assertions" )] pub async fn my_handler( State(state): State, Json(req): Json, ) -> Result<(StatusCode, Json), ApiError> { // Convert DTO -> internal type // Call library code (QueryEngine, Ingestor, etc.) // Convert result -> DTO // Return } ``` Rules: - Handlers are thin: convert types, delegate to library code, convert back - No business logic in handlers - Always return `Result<_, ApiError>` for consistent error responses - Use `tag = "..."` to group related endpoints in docs ### 3. Register the Route Add to the `OpenApiRouter` in `crates/stemedb-api/src/lib.rs`: ```rust let app = OpenApiRouter::new() .route("/my-endpoint", post(my_handler)) // ...existing routes... ``` The `OpenApiRouter` automatically collects the `#[utoipa::path]` metadata. No separate registration step. ### 4. Verify ```bash # Build and test cargo test -p stemedb-api # Check docs generated correctly cargo run -p stemedb-api & curl localhost:18180/api-doc/openapi.json | jq .paths ``` ## Error Responses All error responses use a consistent `ApiError` type that implements `ToSchema`: ```rust #[derive(Serialize, ToSchema)] pub struct ApiError { pub error: String, pub code: String, } ``` Map internal errors to API errors in the handler layer. Never leak internal error details. ## Checklist - [ ] DTO types in `dto.rs` with `Serialize, Deserialize, ToSchema` - [ ] `From<>` conversions between DTOs and internal types - [ ] Handler annotated with `#[utoipa::path]` - [ ] Error responses documented in `responses(...)` - [ ] Route registered on `OpenApiRouter` - [ ] Tests cover happy path and error cases - [ ] `cargo clippy` passes - [ ] Schema examples are meaningful (not "string") ## Troubleshooting ### OpenAPI spec missing my endpoint The handler must have `#[utoipa::path]` AND be registered on `OpenApiRouter` (not a plain `axum::Router`). ### Type not showing in schemas The DTO must derive `ToSchema`. If it references other types, those must also derive `ToSchema`. ### Swagger UI not updating Hard refresh the browser. The spec is regenerated on each startup. ## Related - [ai-lookup/patterns/api-documentation.md](../../../ai-lookup/patterns/api-documentation.md) - Toolchain overview - [ai-lookup/services/api.md](../../../ai-lookup/services/api.md) - Service facts - [Rust Guidelines](./rust-guidelines.md) - General Rust patterns