stemedb/.claude/guides/backend/api-endpoints.md
jordan b3e8a9a058 feat: Multi-application expansion with chaos testing and community UI
Major additions:
- Community Next.js app (port 18187) for browsing claims with API docs
- stemedb-chaos crate: Fault injection, chaos testing, CRDT properties
- Latent ingestion system: Reddit/FDA ingesters with ADK-Go agents
- Disputed claims handling: Manual review workflows and validation
- Aphoria security scanner: New extractors (SQL injection, command
  injection, weak crypto, TLS version), policy-based ignores, UAT reports
- Docker infrastructure: Dockerfile, docker-compose.yml for full stack
- VulnBank demo: Intentionally vulnerable multi-language test corpus

SDK & API enhancements:
- Source registry handlers for tracking data provenance
- Metrics endpoint
- Skeptic filtering improvements

Code quality:
- Split 14 large files (>500 lines) into focused modules
- All files now under 500-line limit per project guidelines

Documentation:
- Chaos testing guide, circuit breakers, observability docs
- Phase 7 UAT documentation updates
- Martin Kleppmann technical writer agent

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 01:24:14 -07:00

3.9 KiB

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 for architecture overview

Quick Start

# 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):

#[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:

#[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

3. Register the Route

Add to the OpenApiRouter in crates/stemedb-api/src/lib.rs:

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

# 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:

#[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.