Rust is Cool. What if we Made it Boring?

Using Rust for REST APIs

Founded a YC company named trieve which was built in Rust. I now work at Mintlify.

1 / 13

API-first companies are huge

  1. Stripe - $91B
  2. Twilio - $20B
  3. ElevenLabs - $6.6B
  4. Plaid - $6.1B
  5. Algolia - $2.25B (my inspiration)
2 / 13

What is a REST API?

RESOURCES (nouns)          ENDPOINTS (verbs + nouns)
─────────────────          ────────────────────────────
    User         ───────►  GET    /api/users
                           POST   /api/users
                           GET    /api/users/{id}
                           PUT    /api/users/{id}
                           DELETE /api/users/{id}
                           ...
3 / 13

What is OpenAPI?

Trieve API Documentation

4 / 13

What is OpenAPI?

OpenAPI Specification

5 / 13

Utoipa

Utoipa is the glue that makes Rust good at REST.

#[derive(OpenApi)]
#[openapi(
    info(
        title = "Trieve API",
        description = "Trieve OpenAPI Specification...",
        contact(name = "Trieve Team", url = "https://trieve.ai"),
        license(name = "BSL", url = "https://github.com/..."),
        version = "0.13.0",
    ),
    servers(
        (url = "https://api.trieve.ai", description = "Production"),
        (url = "http://localhost:8090", description = "Local"),
    ),
    paths(handlers::dataset_handler::create_dataset),
    components(schemas(CreateDatasetReqPayload)),
    tags((name = "Dataset", description = "Dataset endpoint...")),
)]
pub struct ApiDoc;
6 / 13

Utoipa - Serving the Spec

.service(
    SwaggerUi::new("/swagger-ui/{_:.*}")
        .url("/api-docs/openapi.json", ApiDoc::openapi())
)
7 / 13

Utoipa - Route Annotations

/// Create Dataset
///
/// Auth'ed user must be an owner of the organization.
#[utoipa::path(
    post,
    path = "/dataset",
    context_path = "/api",
    tag = "Dataset",
    request_body(content = CreateDatasetReqPayload,
                 description = "JSON request payload"),
    responses(
        (status = 200, description = "Dataset created", body = Dataset),
        (status = 400, description = "Service error", body = ErrorResponseBody),
    ),
    params(("TR-Organization" = Uuid, Header)),
    security(("ApiKey" = ["owner"])),
)]
8 / 13

Architecture

Maintain flexibility for a migration to JsonRPC.

┌─────────────────┐ 
│   HTTP Request  │ 
└────────┬────────┘ 
         │          
         ▼          
┌─────────────────────────────────────────┐
│              HANDLERS                   │
│  (parse input, validate, call operator) │
└────────────────────┬────────────────────┘
                     │
                     ▼
┌─────────────────────────────────────────┐
│              OPERATORS                  │
│   (business logic, DB queries, etc.)    │
└─────────────────────────────────────────┘
9 / 13

OLTP DB ORM

  • diesel is good
  • use diesel-async
pub async fn create_dataset_query(
    new_dataset: Dataset,
    pool: web::Data<Pool>,
) -> Result<Dataset, ServiceError> {
    use crate::data::schema::datasets::dsl::*;

    let mut conn = pool.get().await
        .map_err(|_| ServiceError::BadRequest("Could not get database connection"))?;

    diesel::insert_into(datasets)
        .values(&new_dataset)
        .execute(&mut conn)
        .await
        .map_err(|err| match err {
            Error::DatabaseError(DatabaseErrorKind::UniqueViolation, _) =>
                ServiceError::BadRequest("Dataset tracking_id already exists"),
            _ => ServiceError::BadRequest("Could not create dataset"),
        })?;

    Ok(new_dataset)
}
10 / 13

It's hard to mess up

  • put clippy on extra strict
  • make sure utoipa is accurate
  • don't unwrap
11 / 13

Pro tips

  • marketing, marketing, marketing
match tokio::time::timeout(
    std::time::Duration::from_secs(timeout_secs),
    next.call(service_req),
)
12 / 13

Rust is Cool. What if we Made it Boring?

Using Rust for REST APIs

13 / 13