# 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<stemedb_core::types::X>` 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<AppState>,
    Json(req): Json<MyRequest>,
) -> Result<(StatusCode, Json<MyResponse>), 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

### 2.1. Query Parameters with Arrays

**CRITICAL:** If your query parameter DTO contains `Vec<T>` or `Option<Vec<T>>`, you MUST use `QsQuery` instead of standard `Query`.

#### Use `QsQuery` for Array Parameters ✅

```rust
use crate::extractors::QsQuery;

#[derive(Deserialize, ToSchema)]
pub struct FilterRequest {
    /// List of sources to filter by
    pub sources: Option<Vec<String>>,  // ← Array type
    pub limit: usize,
}

#[utoipa::path(
    get,
    path = "/my-endpoint",
    params(
        ("sources" = Option<Vec<String>>, Query, description = "Filter sources"),
        ("limit" = usize, Query, description = "Max results"),
    ),
    // ...
)]
pub async fn my_handler(
    State(state): State<AppState>,
    QsQuery(params): QsQuery<FilterRequest>,  // ✅ CORRECT
) -> Result<Json<MyResponse>> {
    // Dashboard sends: ?sources[]=rfc&sources[]=community&limit=10
    // params.sources = Some(vec!["rfc", "community"])
}
```

#### Never Use Standard `Query` with Arrays ❌

```rust
// ❌ WRONG - Will silently fail!
pub async fn broken_handler(
    State(state): State<AppState>,
    Query(params): Query<FilterRequest>,  // sources will always be None!
) -> Result<Json<MyResponse>> {
    // Dashboard sends: ?sources[]=rfc
    // Result: params.sources = None (filter doesn't work!)
}
```

#### Use Standard `Query` for Scalar Parameters Only ✅

```rust
use axum::extract::Query;

#[derive(Deserialize, ToSchema)]
pub struct SimpleRequest {
    pub limit: usize,
    pub offset: usize,
    pub category: Option<String>,  // All scalars, no arrays
}

pub async fn simple_handler(
    State(state): State<AppState>,
    Query(params): Query<SimpleRequest>,  // ✅ CORRECT
) -> Result<Json<MyResponse>> {
    // ?limit=10&offset=0&category=security
}
```

**Why:** The StemeDB Dashboard uses JavaScript's `URLSearchParams` which generates bracket notation for arrays (`?filters[]=a&filters[]=b`). Standard `axum::extract::Query` uses `serde_urlencoded` which doesn't support brackets. `QsQuery` uses `serde_qs` which does.

**Quick Check:**
- DTO has `Vec` or `Option<Vec>` field? → Use `QsQuery`
- All fields are scalars? → Use `Query` or `AxumQuery`

See `crates/stemedb-api/src/extractors.rs` for detailed documentation.

### 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]`
- [ ] **Query parameters:** Use `QsQuery` if DTO has `Vec` fields, otherwise use `Query`
- [ ] 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
