---
name: vespera
description: Build APIs with Vespera - FastAPI-like DX for Rust/Axum. Covers route handlers, Schema derivation, and OpenAPI generation.
---

# Vespera Usage Guide

Vespera = FastAPI DX for Rust. Zero-config OpenAPI 3.1 generation via compile-time macro scanning.

## Quick Start

```rust
use vespera::{vespera, Serve, Schema, Validated, axum::Json};
use axum::extract::Path;
use serde::{Deserialize, Serialize};
use garde::Validate;

// 1. Custom types — derive Schema for OpenAPI inclusion.
//    Add `garde::Validate` to opt into 422 validation.
#[derive(Serialize, Deserialize, Schema, Validate)]
pub struct CreateUser {
    #[garde(length(min = 3, max = 32))]
    pub name: String,
    #[garde(email)]
    pub email: String,
}

// 2. Route handlers — MUST be `pub async fn`.
#[vespera::route(get, path = "/{id}", tags = ["users"])]
pub async fn get_user(Path(id): Path<u32>) -> Json<CreateUser> { /* ... */ }

// 3. Validated extractor → automatic 422 on bad input.
#[vespera::route(post, tags = ["users"])]
pub async fn create_user(
    Validated(Json(req)): Validated<Json<CreateUser>>,
) -> Json<&'static str> {
    // `req` already passed validation. Failures never reach here.
    Json("ok")
}

// 4. Main — one-liner `.serve()` from the `Serve` extension trait.
#[tokio::main]
async fn main() -> std::io::Result<()> {
    vespera!(
        openapi  = "openapi.json", // writes file at compile time
        title    = "My API",
        version  = "1.0.0",
        docs_url = "/docs",        // Swagger UI
        redoc_url = "/redoc"       // ReDoc alternative
    )
    .serve("0.0.0.0:3000")
    .await
}
```

---

## Request Validation (`Validated<T>` → `422`)

Wrap any extractor with `Validated<...>` to enforce `garde::Validate` **before**
the handler runs. Vespera converts validation failures into a canonical
`422 Unprocessable Entity` response — no per-handler error mapping.

```rust
use vespera::{Validated, axum::Json};

#[vespera::route(post)]
pub async fn create(
    Validated(Json(req)): Validated<Json<CreateUser>>,
) -> Json<&'static str> {
    Json("ok")
}
```

**Response on validation failure (status `422`, content-type `application/json`):**

```json
{
  "errors": [
    { "path": "name",  "message": "length is lower than 3" },
    { "path": "email", "message": "not a valid email" }
  ]
}
```

### Supported wrappers

| Wrapper | Validates |
|---|---|
| `Validated<Json<T>>` | JSON body |
| `Validated<Form<T>>` | URL-encoded form body |
| `Validated<Query<T>>` | URL query string |
| `Validated<Path<T>>` | Path parameters |

### Requirements

- `T` (or the inner type of `Json<T>`, `Form<T>`, …) must implement
  `garde::Validate<Context = ()>`.
- Derive `garde::Validate` and annotate fields with `#[garde(...)]` rules
  (`length`, `email`, `range`, `pattern`, custom, …).
- Vespera's `#[derive(Schema)]` continues to drive the OpenAPI spec — the two
  derives compose cleanly on the same struct.

### JNI / Binary wire integration

When a `Validated` rejection crosses the JNI boundary, the JSON envelope
(`{"errors":[...]}`) is **hoisted** into the binary wire-format header as
`"validation_errors": [...]`. Java decoders inspect the field directly
without re-parsing the body. See
`crates/vespera/tests/jni_validation.rs` for the pinned contract.

---

## One-Liner Server Startup (`Serve`)

`vespera::Serve` is an extension trait on `axum::Router`. It replaces the
standard `TcpListener::bind` + `axum::serve(...)` dance with a single chained
call:

```rust
use vespera::{vespera, Serve};

#[tokio::main]
async fn main() -> std::io::Result<()> {
    vespera!(title = "My API")
        .serve("0.0.0.0:3000")
        .await
}
```

- `addr` accepts anything `tokio::net::ToSocketAddrs` accepts — strings
  (`"0.0.0.0:3000"`), tuples (`("127.0.0.1", 8080)`), `SocketAddr`, etc.
- Works on **any** `axum::Router`, including the output of `Router::merge`,
  `Router::nest`, or `vespera!(...)` itself.
- Returns `std::io::Result<()>` — propagate with `?` from `main`.

---

## Type Mapping Reference

| Rust Type | OpenAPI Schema | Notes |
|-----------|----------------|-------|
| `String`, `&str` | `string` | |
| `i8`-`i128`, `u8`-`u128` | `integer` | |
| `f32`, `f64` | `number` | |
| `bool` | `boolean` | |
| `Vec<T>` | `array` + items | |
| `BTreeSet<T>`, `HashSet<T>` | `array` + items + `uniqueItems: true` | Set types |
| `Option<T>` | T (nullable context) | Parent marks as optional |
| `HashMap<K,V>` | `object` + additionalProperties | |
| `Uuid` | `string` + `format: uuid` | |
| `Decimal` | `string` + `format: decimal` | |
| `NaiveDate` | `string` + `format: date` | |
| `NaiveTime` | `string` + `format: time` | |
| `DateTime`, `DateTimeWithTimeZone` | `string` + `format: date-time` | |
| `FieldData<NamedTempFile>` | `string` + `format: binary` | File upload field |
| `()` | empty response | 204 No Content |
| Custom struct | `$ref` | Must derive Schema |

## Extractor Mapping Reference

| Axum Extractor | OpenAPI Location | Notes |
|----------------|------------------|-------|
| `Path<T>` | path parameter | T can be tuple or struct |
| `Query<T>` | query parameters | Struct fields become params |
| `Json<T>` | requestBody | application/json |
| `Form<T>` | requestBody | application/x-www-form-urlencoded |
| `TypedMultipart<T>` | requestBody | multipart/form-data — typed with schema |
| `Multipart` | requestBody | multipart/form-data — untyped, generic object |
| `State<T>` | **ignored** | Internal, not API |
| `Extension<T>` | **ignored** | Internal, not API |
| `TypedHeader<T>` | header parameter | |
| `HeaderMap` | **ignored** | Too dynamic |

---

## Route Handler Requirements

```rust
// ❌ Private function - NOT discovered
async fn get_users() -> Json<Vec<User>> { ... }

// ❌ Non-async function - NOT supported
pub fn get_users() -> Json<Vec<User>> { ... }

// ✅ Must be pub async fn
pub async fn get_users() -> Json<Vec<User>> { ... }
```

---

## File Structure → URL Mapping

```
src/routes/
├── mod.rs           → /              (root routes)
├── users.rs         → /users
├── posts.rs         → /posts
└── admin/
    ├── mod.rs       → /admin
    └── stats.rs     → /admin/stats
```

Handler path is: `{file_path} + {#[route] path}`

```rust
// In src/routes/users.rs
#[vespera::route(get, path = "/{id}")]
pub async fn get_user(...) // → GET /users/{id}
```

---

## Serde Integration

Vespera respects serde attributes:

```rust
#[derive(Serialize, Deserialize, Schema)]
#[serde(rename_all = "camelCase")]  // ✅ Respected in schema
pub struct UserResponse {
    user_id: u32,        // → "userId" in JSON Schema
    
    #[serde(rename = "fullName")]  // ✅ Respected
    name: String,        // → "fullName" in JSON Schema
    
    #[serde(default)]    // ✅ Recognized (does NOT affect `required` — only Option<T> does)
    bio: Option<String>,
    
    #[serde(skip)]       // ✅ Excluded from schema
    internal_id: u64,
}
```

---

## Debugging Tips

### Schema Not Appearing

1. Check `#[derive(Schema)]` on the type
2. Check type is used in a route handler's input/output
3. Check for generic types - all type params need Schema

```rust
// Generic types need Schema on all params
#[derive(Schema)]
struct Paginated<T: Schema> {  // T must also derive Schema
    items: Vec<T>,
    total: u32,
}
```

### Macro Expansion

```bash
# See what vespera! generates
cargo expand

# Validate OpenAPI output
npx @apidevtools/swagger-cli validate openapi.json
```

---

## Environment Variables

| Variable | Purpose | Default |
|----------|---------|---------|
| `VESPERA_DIR` | Route folder name | `routes` |
| `VESPERA_OPENAPI` | OpenAPI output path | none |
| `VESPERA_TITLE` | API title | `API` |
| `VESPERA_VERSION` | API version | `CARGO_PKG_VERSION` |
| `VESPERA_DOCS_URL` | Swagger UI path | none |
| `VESPERA_REDOC_URL` | ReDoc path | none |
| `VESPERA_SERVER_URL` | Server URL | `http://localhost:3000` |

---

## schema_type! Macro (RECOMMENDED)

> **ALWAYS prefer `schema_type!` over manually defining request/response structs.**
> 
> Benefits:
> - Single source of truth (your model)
> - Auto-generated `From` impl for easy conversion
> - Automatic type resolution (enums, custom types → absolute paths)
> - SeaORM relation support (HasOne, BelongsTo, HasMany)
> - No manual field synchronization

### Best Practices

| DO | DON'T |
|----|-------|
| Use `pick` to select only needed fields | Define manual structs that duplicate Model fields |
| Use `omit` to exclude sensitive fields | Use `name` parameter unnecessarily |
| Use full `crate::models::...` paths | Rely on implicit module resolution |
| Define schema near route handlers | Scatter schemas across unrelated files |

**Primary Parameters (USE THESE):**
- `pick = [...]` - Allowlist: include ONLY these fields
- `omit = [...]` - Denylist: exclude these fields
- `omit_default` - Auto-omit fields with DB defaults (primary_key, default_value)

**Advanced Parameters (USE SPARINGLY):**
- `partial` - For PATCH endpoints only
- `rename` - Only when API naming differs from model
- `add` - Only when truly new fields needed (breaks `From` impl)
- `name` - **AVOID** unless same-file Model reference (see below)

### Why Not Manual Structs?

```rust
// ❌ BAD: Manual struct definition - requires sync with Model
#[derive(Serialize, Deserialize, Schema)]
pub struct UserResponse {
    pub id: i32,
    pub name: String,
    pub email: String,
    // Forgot to add new field? Schema out of sync!
}

// ✅ GOOD: Derive from Model - always in sync
schema_type!(UserResponse from crate::models::user::Model, omit = ["password_hash"]);
```

### Basic Syntax

```rust
// Pick specific fields
schema_type!(CreateUserRequest from crate::models::user::Model, pick = ["name", "email"]);

// Omit specific fields
schema_type!(UserResponse from crate::models::user::Model, omit = ["password_hash", "internal_id"]);

// Add new fields (NOTE: no From impl generated when using add)
schema_type!(UpdateUserRequest from crate::models::user::Model, pick = ["name"], add = [("id": i32)]);

// Rename fields
schema_type!(UserDTO from crate::models::user::Model, rename = [("id", "user_id")]);

// Partial updates (all fields become Option<T>)
schema_type!(UserPatch from crate::models::user::Model, partial);

// Partial updates (specific fields only)
schema_type!(UserPatch from crate::models::user::Model, partial = ["name", "email"]);

// Auto-omit fields with DB defaults (primary_key, default_value = "...")
schema_type!(CreatePostRequest from crate::models::post::Model, omit_default);

// Combine omit_default with add
schema_type!(CreateItemRequest from crate::models::item::Model, omit_default, add = [("tags": Vec<String>)]);

// Custom serde rename strategy
schema_type!(UserSnakeCase from crate::models::user::Model, rename_all = "snake_case");

// Custom OpenAPI schema name
schema_type!(Schema from Model, name = "UserSchema");

// Skip Schema derive (won't appear in OpenAPI)
schema_type!(InternalDTO from Model, ignore);

// Disable Clone derive
schema_type!(LargeResponse from SomeType, clone = false);
```

### Same-File Model Reference (When to Use `name`)

> **The `name` parameter is ONLY needed for same-file Model references.**
> For cross-file references, use full paths and descriptive struct names instead.

When defining Schema in the same file as Model (common for SeaORM entities):

```rust
// In src/models/user.rs
pub struct Model {
    pub id: i32,
    pub name: String,
    pub status: UserStatus,  // Custom enum - auto-resolved to absolute path
}

pub enum UserStatus { Active, Inactive }

// ✅ CORRECT: Same-file reference - use `name` for OpenAPI schema name
vespera::schema_type!(Schema from Model, name = "UserSchema");

// ❌ WRONG: Using `name` for cross-file reference
// schema_type!(Schema from crate::models::user::Model, name = "UserResponse");
// ✅ CORRECT: Use descriptive struct name instead
// schema_type!(UserResponse from crate::models::user::Model, omit = ["password"]);
```

**Why avoid `name` for cross-file references?**
- The struct name itself becomes the OpenAPI schema name
- `UserResponse` is clearer than `Schema` with `name = "UserResponse"`
- Less parameters = less complexity

### Cross-File References

Reference structs from other files using full module paths:

```rust
// In src/routes/users.rs
use vespera::schema_type;

// Reference model from src/models/user.rs
schema_type!(CreateUserRequest from crate::models::user::Model, pick = ["name", "email"]);
```

The macro reads the source file at compile time - no special annotations needed on the source struct.

### Auto-Generated From Impl

When `add` is NOT used, `schema_type!` generates a `From` impl for easy conversion:

```rust
// This:
schema_type!(UserResponse from crate::models::user::Model, omit = ["password_hash"]);

// Generates:
pub struct UserResponse { id, name, email, created_at }

impl From<crate::models::user::Model> for UserResponse {
    fn from(source: crate::models::user::Model) -> Self {
        Self { id: source.id, name: source.name, ... }
    }
}

// Usage:
let model: Model = db.find_user(id).await?;
Json(model.into())  // Easy conversion!
```

**Note:** `From` is NOT generated when `add` is used (can't auto-populate added fields).

### Parameters

**Recommended (Primary):**

| Parameter | Description | Example |
|-----------|-------------|---------|
| `pick` | Include only these fields | `pick = ["name", "email"]` |
| `omit` | Exclude these fields | `omit = ["password"]` |
| `omit_default` | Auto-omit fields with DB defaults | `omit_default` (bare keyword) |

**Situational (Use When Needed):**

| Parameter | Description | When to Use |
|-----------|-------------|-------------|
| `partial` | Make fields optional | PATCH endpoints only |
| `rename` | Rename fields | API naming differs from model |
| `rename_all` | Serde rename strategy | Different casing needed |
| `add` | Add new fields | New fields not in model (breaks `From` impl) |
| `multipart` | Derive `Multipart` | Multipart form-data endpoints |

**Avoid (Special Cases Only):**

| Parameter | Description | When to Use |
|-----------|-------------|-------------|
| `name` | Custom OpenAPI schema name | **Same-file Model reference only** |
| `ignore` | Skip Schema derive | Internal DTOs not for OpenAPI |
| `clone` | Control Clone derive | Large structs where Clone is expensive |

### SeaORM Integration (RECOMMENDED)

`schema_type!` has first-class SeaORM support with automatic relation handling:

```rust
// src/models/memo.rs
#[derive(Clone, Debug, DeriveEntityModel)]
#[sea_orm(table_name = "memo")]
pub struct Model {
    #[sea_orm(primary_key)]
    pub id: i32,
    pub title: String,
    pub user_id: i32,
    pub status: MemoStatus,                      // Custom enum
    pub user: BelongsTo<super::user::Entity>,    // → Option<Box<UserSchema>>
    pub comments: HasMany<super::comment::Entity>, // → Vec<CommentSchema>
    pub created_at: DateTimeWithTimeZone,        // → chrono::DateTime<FixedOffset>
}

#[derive(EnumIter, DeriveActiveEnum, Serialize, Deserialize, Schema)]
pub enum MemoStatus { Draft, Published, Archived }

// Generates Schema with proper types - no imports needed!
vespera::schema_type!(Schema from Model, name = "MemoSchema");
```

**Automatic Type Conversions:**

| SeaORM Type | Generated Type | Notes |
|-------------|---------------|-------|
| `HasOne<Entity>` | `Box<Schema>` or `Option<Box<Schema>>` | Based on FK nullability |
| `BelongsTo<Entity>` | `Option<Box<Schema>>` | Always optional |
| `HasMany<Entity>` | `Vec<Schema>` | |
| `DateTimeWithTimeZone` | `vespera::chrono::DateTime<FixedOffset>` | No SeaORM import needed |
| Custom enums | `crate::module::EnumName` | Auto-resolved to absolute path |

**Circular Reference Handling:** Automatically detected and handled by inlining fields.

**Database Defaults in OpenAPI:** Fields with `#[sea_orm(default_value = "...")]` or `#[sea_orm(primary_key)]` automatically get `default` values in the generated OpenAPI schema. SQL functions like `NOW()` and `gen_random_uuid()` are mapped to type-appropriate defaults.

**Required Logic:** `required` is determined **solely by nullability** (`Option<T>`). Fields with `#[serde(default)]` or `#[serde(skip_serializing_if)]` are still `required` unless they are `Option<T>`.

### Same-File Relation Adapters

When a route file defines a local response DTO for a relation, Vespera can preserve unchanged handler code while still generating the right OpenAPI.

Example:

```rust
#[derive(Serialize, vespera::Schema)]
#[serde(rename_all = "camelCase")]
pub struct UserInArticle {
    pub id: Uuid,
    pub name: String,
    pub email: String,
    pub profile_image: Option<String>,
}

#[derive(Serialize, vespera::Schema)]
#[serde(rename_all = "camelCase")]
pub struct CategoryInArticle {
    pub id: i64,
    pub name: String,
    pub parent_category_id: Option<i64>,
    pub is_active: bool,
    pub is_menu: bool,
}

schema_type!(
    ArticleResponse from crate::models::article::Model,
    add = [("article_review_users": Vec<ArticleReviewUserInArticle>)]
);

Ok(ArticleResponse {
    user: user.into(),
    category: category.into(),
    article_review_users,
    ..
})
```

Rules:

- Only applies to single-value relations (`HasOne` / `BelongsTo`)
- The local DTO name must follow `{RelationNamePascal}In{ResponseBase}`
  - `user` on `ArticleResponse` → `UserInArticle`
  - `category` on `ArticleResponse` → `CategoryInArticle`
- Vespera generates local compile adapters so `Option<Model>.into()` works without changing the route
- The adapter wrapper is hidden from OpenAPI; the spec still references the original related schema (`UserSchema`, `CategorySchema`)
- `HasMany` relations remain excluded by default unless explicitly `pick`ed or `add`ed

### Complete Example

```rust
// ============================================
// src/models/user.rs (SeaORM entity)
// ============================================
#[derive(Clone, Debug, DeriveEntityModel, Serialize, Deserialize)]
#[sea_orm(table_name = "users")]
pub struct Model {
    #[sea_orm(primary_key)]
    pub id: i32,
    pub name: String,
    pub email: String,
    pub status: UserStatus,
    pub password_hash: String,  // Never expose!
    pub created_at: DateTimeWithTimeZone,
}

// ✅ Same-file: use `name` parameter for OpenAPI schema name
vespera::schema_type!(Schema from Model, name = "UserSchema");

// ============================================
// src/routes/users.rs (Route handlers)
// ============================================
use vespera::schema_type;

// ✅ Cross-file: use descriptive struct names + pick/omit
// NO `name` parameter needed - struct name = OpenAPI schema name
schema_type!(CreateUserRequest from crate::models::user::Model, pick = ["name", "email"]);
schema_type!(UserResponse from crate::models::user::Model, omit = ["password_hash"]);
schema_type!(UserPatch from crate::models::user::Model, omit = ["password_hash", "id"], partial);

#[vespera::route(get, path = "/{id}")]
pub async fn get_user(Path(id): Path<i32>, State(db): State<DbPool>) -> Json<UserResponse> {
    let user = User::find_by_id(id).one(&db).await.unwrap().unwrap();
    Json(user.into())  // From impl handles conversion
}

#[vespera::route(patch, path = "/{id}")]
pub async fn patch_user(
    Path(id): Path<i32>,
    Json(patch): Json<UserPatch>,  // All fields are Option<T>
) -> Json<UserResponse> {
    // Apply partial update...
}
```

### Multipart Mode (`multipart`)

Generate `Multipart` structs from existing multipart request types:

```rust
use vespera::multipart::{FieldData, TypedMultipart};
use vespera::{Multipart, Schema};
use tempfile::NamedTempFile;

// Base multipart struct (manually defined)
#[derive(Multipart, Schema)]
pub struct CreateUploadRequest {
    pub name: String,
    #[form_data(limit = "10MiB")]
    pub thumbnail: Option<FieldData<NamedTempFile>>,
    #[form_data(limit = "50MiB")]
    pub document: Option<FieldData<NamedTempFile>>,
    pub tags: Option<String>,
}

// Derive a partial update struct via schema_type!
// - Derives Multipart (not serde)
// - All fields become Option<T> (partial)
// - "document" field excluded
// - #[form_data(limit = "10MiB")] preserved from source
schema_type!(PatchUploadRequest from CreateUploadRequest, multipart, partial, omit = ["document"]);
```

**What `multipart` mode changes:**

| Aspect | Normal Mode | Multipart Mode |
|--------|------------|----------------|
| Derives | `Serialize`, `Deserialize` | `Multipart` |
| Struct attrs | `#[serde(rename_all=...)]` | None |
| Field attrs | `#[serde(...)]` preserved | `#[form_data(...)]` preserved |
| Relation fields | Included (BelongsTo/HasOne) | **Skipped** (can't represent in forms) |
| `From` impl | Auto-generated | **Not generated** |

**OpenAPI rename alignment:** The schema parser reads `#[form_data(field_name = "...")]` and `#[serde(rename_all = "...")]` for multipart structs, ensuring OpenAPI field names match runtime multipart parsing.

**Dependencies required in your Cargo.toml:**
```toml
vespera = "0.1"                # Includes multipart support natively
tempfile = "3"                 # For NamedTempFile file uploads
```

### Quick Reference

```rust
// ✅ RECOMMENDED PATTERNS
schema_type!(CreateUserRequest from crate::models::user::Model, pick = ["name", "email"]);
schema_type!(CreatePostRequest from crate::models::post::Model, omit_default);
schema_type!(UserResponse from crate::models::user::Model, omit = ["password_hash"]);
schema_type!(UserListItem from crate::models::user::Model, pick = ["id", "name"]);

// ✅ MULTIPART PATTERNS
schema_type!(PatchUpload from CreateUploadRequest, multipart, partial);
schema_type!(SmallUpload from CreateUploadRequest, multipart, omit = ["document"]);

// ⚠️ USE SPARINGLY
schema_type!(UserPatch from crate::models::user::Model, partial);  // PATCH only
schema_type!(Schema from Model, name = "UserSchema");              // Same-file only

// ❌ AVOID
schema_type!(Schema from crate::models::user::Model, name = "UserResponse");  // Use struct name!
```

---

## Merging Multiple Vespera Apps

Combine routes and OpenAPI specs from multiple apps at compile time.

### export_app! Macro

Export an app for merging:

```rust
// Child crate (e.g., third/src/lib.rs)
mod routes;

// Basic - scans "routes" folder by default
vespera::export_app!(ThirdApp);

// Custom directory
vespera::export_app!(ThirdApp, dir = "api");
```

Generates:
- `ThirdApp::OPENAPI_SPEC: &'static str` - OpenAPI JSON
- `ThirdApp::router() -> Router` - Axum router

### merge Parameter

Merge child apps in parent:

```rust
let app = vespera!(
    openapi = "openapi.json",
    docs_url = "/docs",
    merge = [third::ThirdApp, other::OtherApp]
)
.with_state(state);
```

**What happens:**
1. Child routers merged into parent router
2. OpenAPI specs merged (paths, schemas, tags)
3. Swagger UI shows all routes

### How It Works (Compile-Time)

```
Child compilation (export_app!):
  1. Scan routes/ folder
  2. Generate OpenAPI spec
  3. Write to target/vespera/{Name}.openapi.json

Parent compilation (vespera! with merge):
  1. Generate parent OpenAPI spec
  2. Read child specs from target/vespera/
  3. Merge all specs together
  4. Write merged openapi.json
```
