{"id":34253638,"url":"https://github.com/dev-five-git/vespera","last_synced_at":"2026-04-02T11:55:42.024Z","repository":{"id":325961994,"uuid":"1103050809","full_name":"dev-five-git/vespera","owner":"dev-five-git","description":"A fully automated OpenAPI engine for Axum with zero-config route and schema discovery","archived":false,"fork":false,"pushed_at":"2026-04-01T14:41:36.000Z","size":1951,"stargazers_count":25,"open_issues_count":0,"forks_count":2,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-01T16:24:35.916Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dev-five-git.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null},"funding":{"github":"dev-five-git","patreon":"JeongMinOh"}},"created_at":"2025-11-24T11:14:46.000Z","updated_at":"2026-04-01T14:36:58.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/dev-five-git/vespera","commit_stats":null,"previous_names":["dev-five-git/vespera"],"tags_count":116,"template":false,"template_full_name":null,"purl":"pkg:github/dev-five-git/vespera","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dev-five-git%2Fvespera","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dev-five-git%2Fvespera/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dev-five-git%2Fvespera/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dev-five-git%2Fvespera/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dev-five-git","download_url":"https://codeload.github.com/dev-five-git/vespera/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dev-five-git%2Fvespera/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31305939,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-02T09:48:21.550Z","status":"ssl_error","status_checked_at":"2026-04-02T09:48:19.196Z","response_time":89,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-12-16T11:48:30.179Z","updated_at":"2026-04-02T11:55:42.017Z","avatar_url":"https://github.com/dev-five-git.png","language":"Rust","funding_links":["https://github.com/sponsors/dev-five-git","https://patreon.com/JeongMinOh"],"categories":[],"sub_categories":[],"readme":"# Vespera\n\n**FastAPI-like developer experience for Rust.** Zero-config OpenAPI 3.1 generation for Axum.\n\n[![Crates.io](https://img.shields.io/crates/v/vespera.svg)](https://crates.io/crates/vespera)\n[![Documentation](https://docs.rs/vespera/badge.svg)](https://docs.rs/vespera)\n[![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)\n[![CI](https://img.shields.io/github/actions/workflow/status/dev-five-git/vespera/CI.yml?branch=main\u0026label=CI)](https://github.com/dev-five-git/vespera/actions)\n[![Codecov](https://img.shields.io/codecov/c/github/dev-five-git/vespera)](https://codecov.io/gh/dev-five-git/vespera)\n\n```rust\n// That's it. Swagger UI at /docs, OpenAPI at openapi.json\nlet app = vespera!(openapi = \"openapi.json\", docs_url = \"/docs\");\n```\n\n## Why Vespera?\n\n| Feature | Vespera | Manual Approach |\n|---------|---------|-----------------|\n| Route registration | Automatic (file-based) | Manual `Router::new().route(...)` |\n| OpenAPI spec | Generated at compile time | Hand-written or runtime generation |\n| Schema extraction | From Rust types | Manual JSON Schema |\n| Swagger UI | Built-in | Separate setup |\n| Type safety | Compile-time verified | Runtime errors |\n\n## Quick Start\n\n### 1. Add Dependencies\n\n```toml\n[dependencies]\nvespera = \"0.1\"\naxum = \"0.8\"\ntokio = { version = \"1\", features = [\"full\"] }\nserde = { version = \"1\", features = [\"derive\"] }\n```\n\n### 2. Create Route Handler\n\n```\nsrc/\n├── main.rs\n└── routes/\n    └── users.rs\n```\n\n**`src/routes/users.rs`**:\n```rust\nuse axum::{Json, Path};\nuse serde::{Deserialize, Serialize};\nuse vespera::Schema;\n\n#[derive(Serialize, Deserialize, Schema)]\npub struct User {\n    pub id: u32,\n    pub name: String,\n}\n\n/// Get user by ID\n#[vespera::route(get, path = \"/{id}\", tags = [\"users\"])]\npub async fn get_user(Path(id): Path\u003cu32\u003e) -\u003e Json\u003cUser\u003e {\n    Json(User { id, name: \"Alice\".into() })\n}\n\n/// Create a new user\n#[vespera::route(post, tags = [\"users\"])]\npub async fn create_user(Json(user): Json\u003cUser\u003e) -\u003e Json\u003cUser\u003e {\n    Json(user)\n}\n```\n\n### 3. Setup Main\n\n**`src/main.rs`**:\n```rust\nuse vespera::vespera;\n\n#[tokio::main]\nasync fn main() {\n    let app = vespera!(\n        openapi = \"openapi.json\",\n        title = \"My API\",\n        docs_url = \"/docs\"\n    );\n\n    let listener = tokio::net::TcpListener::bind(\"0.0.0.0:3000\").await.unwrap();\n    println!(\"Swagger UI: http://localhost:3000/docs\");\n    axum::serve(listener, app).await.unwrap();\n}\n```\n\n### 4. Run\n\n```bash\ncargo run\n# Open http://localhost:3000/docs\n```\n\n---\n\n## Core Concepts\n\n### File-Based Routing\n\nFile structure maps to URL paths automatically:\n\n```\nsrc/routes/\n├── mod.rs           → /\n├── users.rs         → /users\n├── posts.rs         → /posts\n└── admin/\n    ├── mod.rs       → /admin\n    └── stats.rs     → /admin/stats\n```\n\n### Route Handlers\n\nHandlers must be `pub async fn` with the `#[vespera::route]` attribute:\n\n```rust\n// GET /users (default method)\n#[vespera::route]\npub async fn list_users() -\u003e Json\u003cVec\u003cUser\u003e\u003e { ... }\n\n// POST /users\n#[vespera::route(post)]\npub async fn create_user(Json(user): Json\u003cUser\u003e) -\u003e Json\u003cUser\u003e { ... }\n\n// GET /users/{id}\n#[vespera::route(get, path = \"/{id}\")]\npub async fn get_user(Path(id): Path\u003cu32\u003e) -\u003e Json\u003cUser\u003e { ... }\n\n// Full options\n#[vespera::route(put, path = \"/{id}\", tags = [\"users\"], description = \"Update user\")]\npub async fn update_user(...) -\u003e ... { ... }\n```\n\n### Schema Derivation\n\nDerive `Schema` on types used in request/response bodies:\n\n```rust\n#[derive(Serialize, Deserialize, vespera::Schema)]\n#[serde(rename_all = \"camelCase\")]  // Serde attributes are respected\npub struct CreateUserRequest {\n    pub user_name: String,          // → \"userName\" in OpenAPI\n    pub email: String,\n    #[serde(default)]\n    pub bio: Option\u003cString\u003e,        // Optional field\n}\n```\n\n### Supported Extractors\n\n| Extractor | OpenAPI Mapping |\n|-----------|-----------------|\n| `Path\u003cT\u003e` | Path parameters |\n| `Query\u003cT\u003e` | Query parameters |\n| `Json\u003cT\u003e` | Request body (application/json) |\n| `Form\u003cT\u003e` | Request body (application/x-www-form-urlencoded) |\n| `TypedMultipart\u003cT\u003e` | Request body (multipart/form-data) — typed with schema |\n| `Multipart` | Request body (multipart/form-data) — untyped, generic object |\n| `TypedHeader\u003cT\u003e` | Header parameters |\n| `State\u003cT\u003e` | Ignored (internal) |\n\n### Multipart Form Data\n\n#### Typed Multipart (Recommended)\n\nUpload files using vespera's built-in `TypedMultipart` extractor:\n\n```rust\nuse vespera::multipart::{FieldData, TypedMultipart};\nuse vespera::{Multipart, Schema};\nuse tempfile::NamedTempFile;\n\n#[derive(Multipart, Schema)]\npub struct CreateUploadRequest {\n    pub name: String,\n    #[form_data(limit = \"10MiB\")]\n    pub file: Option\u003cFieldData\u003cNamedTempFile\u003e\u003e,\n}\n\n#[vespera::route(post, tags = [\"uploads\"])]\npub async fn create_upload(\n    TypedMultipart(req): TypedMultipart\u003cCreateUploadRequest\u003e,\n) -\u003e Json\u003cUploadResponse\u003e { ... }\n```\n\nVespera automatically generates `multipart/form-data` content type in OpenAPI, and maps `FieldData\u003cNamedTempFile\u003e` to `{ \"type\": \"string\", \"format\": \"binary\" }`.\n\n#### Raw Multipart (Untyped)\n\nFor dynamic multipart handling where the fields aren't known at compile time, use axum's built-in `Multipart` extractor:\n\n```rust\nuse axum::extract::Multipart;\n\n#[vespera::route(post, tags = [\"uploads\"])]\npub async fn upload(mut multipart: Multipart) -\u003e Json\u003cUploadResponse\u003e {\n    while let Some(field) = multipart.next_field().await.unwrap() {\n        let name = field.name().unwrap_or(\"unknown\").to_string();\n        let data = field.bytes().await.unwrap();\n        // Process each field dynamically...\n    }\n    Json(UploadResponse { success: true })\n}\n```\n\nThis generates a `multipart/form-data` request body with a generic `{ \"type\": \"object\" }` schema in OpenAPI, since the fields are not statically known.\n\n### Error Handling\n\n```rust\n#[derive(Serialize, Schema)]\npub struct ApiError {\n    pub message: String,\n}\n\n#[vespera::route(get, path = \"/{id}\")]\npub async fn get_user(Path(id): Path\u003cu32\u003e) -\u003e Result\u003cJson\u003cUser\u003e, (StatusCode, Json\u003cApiError\u003e)\u003e {\n    if id == 0 {\n        return Err((StatusCode::NOT_FOUND, Json(ApiError { message: \"Not found\".into() })));\n    }\n    Ok(Json(User { id, name: \"Alice\".into() }))\n}\n```\n\n---\n\n## `vespera!` Macro Reference\n\n```rust\nlet app = vespera!(\n    dir = \"routes\",                    // Route folder (default: \"routes\")\n    openapi = \"openapi.json\",          // Output path (writes file at compile time)\n    title = \"My API\",                  // OpenAPI info.title\n    version = \"1.0.0\",                 // OpenAPI info.version (default: CARGO_PKG_VERSION)\n    docs_url = \"/docs\",                // Swagger UI endpoint\n    redoc_url = \"/redoc\",              // ReDoc endpoint\n    servers = [                        // OpenAPI servers\n        { url = \"https://api.example.com\", description = \"Production\" },\n        { url = \"http://localhost:3000\", description = \"Development\" }\n    ],\n    merge = [crate1::App1, crate2::App2]  // Merge child vespera apps\n);\n```\n\n## `export_app!` Macro Reference\n\nExport a vespera app for merging into other apps:\n\n```rust\n// Basic usage (scans \"routes\" folder by default)\nvespera::export_app!(MyApp);\n\n// Custom directory\nvespera::export_app!(MyApp, dir = \"api\");\n```\n\nGenerates a struct with:\n- `MyApp::OPENAPI_SPEC: \u0026'static str` - The OpenAPI JSON spec\n- `MyApp::router() -\u003e Router` - Function returning the Axum router\n\n### Environment Variable Fallbacks\n\nAll parameters support environment variable fallbacks:\n\n| Parameter | Environment Variable |\n|-----------|---------------------|\n| `dir` | `VESPERA_DIR` |\n| `openapi` | `VESPERA_OPENAPI` |\n| `title` | `VESPERA_TITLE` |\n| `version` | `VESPERA_VERSION` |\n| `docs_url` | `VESPERA_DOCS_URL` |\n| `redoc_url` | `VESPERA_REDOC_URL` |\n| `servers` | `VESPERA_SERVER_URL` + `VESPERA_SERVER_DESCRIPTION` |\n\n**Priority**: Macro parameter \u003e Environment variable \u003e Default\n\n---\n\n## `schema_type!` Macro\n\nGenerate request/response types from existing structs. Perfect for creating API types from database models.\n\n### Basic Usage\n\n```rust\nuse vespera::schema_type;\n\n// Pick specific fields only\nschema_type!(CreateUserRequest from crate::models::user::Model, pick = [\"name\", \"email\"]);\n\n// Omit specific fields  \nschema_type!(UserResponse from crate::models::user::Model, omit = [\"password_hash\"]);\n\n// Add new fields\nschema_type!(UpdateUserRequest from crate::models::user::Model, pick = [\"name\"], add = [(\"id\": i32)]);\n```\n\n### Same-File Model Reference\n\nWhen the model is in the same file, you can use a simple name with `name` parameter:\n\n```rust\n// In src/models/user.rs\npub struct Model {\n    pub id: i32,\n    pub name: String,\n    pub email: String,\n}\n\n// Simple `Model` path works when using `name` parameter\nvespera::schema_type!(Schema from Model, name = \"UserSchema\");\n```\n\nThe macro infers the module path from the file location, so relation types like `HasOne\u003csuper::user::Entity\u003e` are resolved correctly.\n\n### Cross-File References\n\nReference structs from other files using full module paths:\n\n```rust\n// In src/routes/users.rs - references src/models/user.rs\nschema_type!(UserResponse from crate::models::user::Model, omit = [\"password_hash\"]);\n```\n\n### Auto-Generated `From` Impl\n\nWhen `add` is NOT used, a `From` impl is automatically generated:\n\n```rust\nschema_type!(UserResponse from crate::models::user::Model, omit = [\"password_hash\"]);\n\n// Now you can do:\nlet model: Model = db.find_user(id).await?;\nJson(model.into())  // Automatic conversion!\n```\n\n### Partial Updates (PATCH)\n\nUse `partial` to make fields optional for PATCH-style updates:\n\n```rust\n// All fields become Option\u003cT\u003e\nschema_type!(UserPatch from User, partial);\n\n// Only specific fields become Option\u003cT\u003e\nschema_type!(UserPatch from User, partial = [\"name\", \"email\"]);\n```\n\n### Serde Rename All\n\nApply serde rename_all strategy:\n\n```rust\n// Convert field names to camelCase in JSON\nschema_type!(UserDTO from User, rename_all = \"camelCase\");\n\n// Available: \"camelCase\", \"snake_case\", \"PascalCase\", \"SCREAMING_SNAKE_CASE\", etc.\n```\n\n### Omit Fields with Database Defaults (`omit_default`)\n\nAutomatically omit fields that have database-level defaults — perfect for create DTOs where the database handles `id`, `created_at`, etc.:\n\n```rust\n#[derive(DeriveEntityModel)]\n#[sea_orm(table_name = \"posts\")]\npub struct Model {\n    #[sea_orm(primary_key)]             // ← has default (auto-increment)\n    pub id: i32,\n    pub title: String,\n    pub content: String,\n    #[sea_orm(default_value = \"NOW()\")]  // ← has default (SQL function)\n    pub created_at: DateTimeWithTimeZone,\n}\n\n// Omits `id` (primary_key) and `created_at` (default_value) automatically\nschema_type!(CreatePostRequest from crate::models::post::Model, omit_default);\n// Generated struct only has: title, content\n```\n\n`omit_default` detects fields with:\n- `#[sea_orm(primary_key)]` — auto-increment / generated IDs\n- `#[sea_orm(default_value = \"...\")]` — SQL defaults like `NOW()`, `gen_random_uuid()`, literals\n\nCan be combined with other parameters:\n\n```rust\n// omit_default + add extra fields\nschema_type!(CreateItemRequest from Model, omit_default, add = [(\"tags\": Vec\u003cString\u003e)]);\n```\n\n### Database Defaults in OpenAPI\n\nFields with database defaults automatically get `default` values in the generated OpenAPI schema:\n\n| SeaORM Attribute | OpenAPI Default |\n|-----------------|-----------------|\n| `primary_key` (Uuid) | `\"00000000-0000-0000-0000-000000000000\"` |\n| `primary_key` (i32/i64) | `0` |\n| `default_value = \"NOW()\"` | `\"1970-01-01T00:00:00+00:00\"` |\n| `default_value = \"gen_random_uuid()\"` | `\"00000000-0000-0000-0000-000000000000\"` |\n| `default_value = \"true\"` | `true` (literal passthrough) |\n\n\u003e **Note:** `required` is determined solely by nullability (`Option\u003cT\u003e`). Fields with defaults are still `required` unless they are `Option\u003cT\u003e`.\n\n### SeaORM Integration\n\n`schema_type!` has first-class support for SeaORM models with relations:\n\n```rust\nuse sea_orm::entity::prelude::*;\n\n#[derive(Clone, Debug, DeriveEntityModel)]\n#[sea_orm(table_name = \"memos\")]\npub struct Model {\n    #[sea_orm(primary_key)]\n    pub id: i32,\n    pub title: String,\n    pub user_id: i32,\n    pub user: BelongsTo\u003csuper::user::Entity\u003e,     // → Option\u003cBox\u003cUserSchema\u003e\u003e\n    pub comments: HasMany\u003csuper::comment::Entity\u003e, // → Vec\u003cCommentSchema\u003e\n}\n\n// Generates Schema with proper relation types\nvespera::schema_type!(Schema from Model, name = \"MemoSchema\");\n```\n\n**Relation Type Conversions:**\n\n| SeaORM Type | Generated Schema Type |\n|-------------|----------------------|\n| `HasOne\u003cEntity\u003e` | `Box\u003cSchema\u003e` or `Option\u003cBox\u003cSchema\u003e\u003e` |\n| `BelongsTo\u003cEntity\u003e` | `Option\u003cBox\u003cSchema\u003e\u003e` |\n| `HasMany\u003cEntity\u003e` | `Vec\u003cSchema\u003e` |\n| `DateTimeWithTimeZone` | `chrono::DateTime\u003cFixedOffset\u003e` |\n\n**Circular Reference Handling:** When schemas reference each other (e.g., User ↔ Memo), the macro automatically detects and handles circular references by inlining fields to prevent infinite recursion.\n\n### Multipart Mode\n\nGenerate `Multipart` structs from existing types using the `multipart` keyword:\n\n```rust\n#[derive(vespera::Multipart, vespera::Schema)]\npub struct CreateUploadRequest {\n    pub name: String,\n    #[form_data(limit = \"10MiB\")]\n    pub file: Option\u003cFieldData\u003cNamedTempFile\u003e\u003e,\n    pub description: Option\u003cString\u003e,\n}\n\n// Generates a Multipart struct (no serde derives), all fields Optional\nschema_type!(PatchUploadRequest from CreateUploadRequest, multipart, partial, omit = [\"file\"]);\n```\n\nWhen `multipart` is enabled:\n- Derives `Multipart` instead of `Serialize`/`Deserialize`\n- Suppresses `#[serde(...)]` attributes (multipart parsing is not serde-based)\n- Preserves `#[form_data(...)]` attributes from source struct\n- Skips SeaORM relation fields (nested objects can't be represented in multipart forms)\n- Does not generate `From` impl\n\n### Parameters\n\n| Parameter | Description |\n|-----------|-------------|\n| `pick` | Include only specified fields |\n| `omit` | Exclude specified fields |\n| `rename` | Rename fields: `rename = [(\"old\", \"new\")]` |\n| `add` | Add new fields (disables auto `From` impl) |\n| `clone` | Control Clone derive (default: true) |\n| `partial` | Make fields optional: `partial` or `partial = [\"field1\"]` |\n| `name` | Custom OpenAPI schema name: `name = \"UserSchema\"` |\n| `rename_all` | Serde rename strategy: `rename_all = \"camelCase\"` |\n| `ignore` | Skip Schema derive (bare keyword, no value) |\n| `multipart` | Derive `Multipart` instead of serde (bare keyword) |\n| `omit_default` | Auto-omit fields with DB defaults: `primary_key`, `default_value` (bare keyword) |\n\n---\n\n## `schema!` Macro\n\nGet a `Schema` value at runtime with optional field filtering. Useful for programmatic schema access.\n\n```rust\nuse vespera::{Schema, schema};\n\n#[derive(Schema)]\npub struct User {\n    pub id: i32,\n    pub name: String,\n    pub password: String,\n}\n\n// Full schema\nlet full: vespera::schema::Schema = schema!(User);\n\n// With fields omitted\nlet safe: vespera::schema::Schema = schema!(User, omit = [\"password\"]);\n\n// With only specified fields\nlet summary: vespera::schema::Schema = schema!(User, pick = [\"id\", \"name\"]);\n```\n\n\u003e **Note:** For creating request/response types, use `schema_type!` instead - it generates actual struct types with `From` impl.\n\n---\n\n## Cron Jobs\n\nSchedule background tasks with `#[vespera::cron]`. Uses [tokio-cron-scheduler](https://crates.io/crates/tokio-cron-scheduler) under the hood.\n\n### Enable Feature\n\n```toml\n[dependencies]\nvespera = { version = \"0.1\", features = [\"cron\"] }\n```\n\n### Define Cron Jobs\n\nPlace `#[vespera::cron(\"...\")]` on any `pub async fn` with zero parameters. The function can live anywhere in your project — no special directory required.\n\n```rust\n// src/cron/cleanup.rs, src/tasks.rs, or even src/routes/users.rs — anywhere works\n#[vespera::cron(\"1/10 * * * * *\")]\npub async fn cleanup_sessions() {\n    println!(\"Running cleanup every 10 seconds\");\n}\n\n#[vespera::cron(\"0 0 * * * *\")]\npub async fn hourly_report() {\n    println!(\"Running hourly report\");\n}\n```\n\n### How It Works\n\n1. `#[cron(\"...\")]` registers the job at compile time (like `#[route]`)\n2. `vespera!()` auto-discovers all registered cron jobs — no extra parameters needed\n3. A background scheduler spawns via `tokio::spawn` when the app starts\n\n```rust\n// No cron-specific config — just works\nlet app = vespera!(docs_url = \"/docs\");\n```\n\n### Cron Expression Format\n\nUses 6-field cron expressions (`sec min hour day month weekday`):\n\n| Expression | Schedule |\n|-----------|----------|\n| `0 */5 * * * *` | Every 5 minutes |\n| `0 0 * * * *` | Every hour |\n| `0 0 0 * * *` | Daily at midnight |\n| `1/10 * * * * *` | Every 10 seconds |\n| `0 30 9 * * Mon-Fri` | Weekdays at 9:30 AM |\n\n### Requirements\n\n- Functions must be `pub async fn`\n- Functions must take **no parameters** (no `State`, no extractors)\n- The `cron` feature must be enabled\n\n---\n\n## Advanced Usage\n\n### Adding State\n\n```rust\nlet app = vespera!(docs_url = \"/docs\")\n    .with_state(AppState { db: pool });\n```\n\n### Adding Middleware\n\n```rust\nlet app = vespera!(docs_url = \"/docs\")\n    .layer(CorsLayer::permissive())\n    .layer(TraceLayer::new_for_http());\n```\n\n### Multiple OpenAPI Files\n\n```rust\nlet app = vespera!(\n    openapi = [\"openapi.json\", \"docs/api-spec.json\"]\n);\n```\n\n### Custom Route Folder\n\n```rust\n// Scans src/api/ instead of src/routes/\nlet app = vespera!(\"api\");\n\n// Or explicitly\nlet app = vespera!(dir = \"api\");\n```\n\n### Merging Multiple Vespera Apps\n\nCombine routes and OpenAPI specs from multiple vespera apps at compile time:\n\n**Child app (e.g., `third` crate):**\n```rust\n// src/lib.rs\nmod routes;\n\n// Export app for merging (dir defaults to \"routes\")\nvespera::export_app!(ThirdApp);\n\n// Or with custom directory\n// vespera::export_app!(ThirdApp, dir = \"api\");\n```\n\n**Parent app:**\n```rust\n// src/main.rs\nuse vespera::vespera;\n\nlet app = vespera!(\n    openapi = \"openapi.json\",\n    docs_url = \"/docs\",\n    merge = [third::ThirdApp]  // Merges router AND OpenAPI spec\n)\n.with_state(app_state);\n```\n\nThis automatically:\n- Merges all routes from child apps into the parent router\n- Combines OpenAPI specs (paths, schemas, tags) into a single spec\n- Makes Swagger UI show all routes from all apps\n\n---\n\n## Type Mapping\n\n| Rust Type | OpenAPI Schema |\n|-----------|----------------|\n| `String`, `\u0026str` | `string` |\n| `i32`, `u64`, etc. | `integer` |\n| `f32`, `f64` | `number` |\n| `bool` | `boolean` |\n| `Vec\u003cT\u003e` | `array` with items |\n| `Option\u003cT\u003e` | nullable T |\n| `HashMap\u003cK, V\u003e` | `object` with additionalProperties |\n| `BTreeSet\u003cT\u003e`, `HashSet\u003cT\u003e` | `array` with `uniqueItems: true` |\n| `Uuid` | `string` with `format: uuid` |\n| `Decimal` | `string` with `format: decimal` |\n| `NaiveDate` | `string` with `format: date` |\n| `NaiveTime` | `string` with `format: time` |\n| `DateTime`, `DateTimeWithTimeZone` | `string` with `format: date-time` |\n| `FieldData\u003cNamedTempFile\u003e` | `string` with `format: binary` |\n| Custom struct | `$ref` to components/schemas |\n\n---\n\n## Project Structure\n\n```\nvespera/\n├── crates/\n│   ├── vespera/           # Main crate - re-exports everything\n│   ├── vespera_core/      # OpenAPI types and abstractions\n│   └── vespera_macro/     # Proc-macros (compile-time magic)\n└── examples/\n    └── axum-example/      # Complete example application\n```\n\n---\n\n## Contributing\n\n```bash\ngit clone https://github.com/dev-five-git/vespera.git\ncd vespera\n\n# Build \u0026 test\ncargo build\ncargo test --workspace\n\n# Run example\ncd examples/axum-example\ncargo run\n# → http://localhost:3000/docs\n```\n\nSee [SKILL.md](./SKILL.md) for development guidelines and architecture details.\n\n---\n\n## Comparison\n\n### vs. utoipa\n\n- **Vespera**: Zero-config, file-based routing, compile-time generation\n- **utoipa**: Manual annotations, more control, works with any router\n\n### vs. aide\n\n- **Vespera**: Automatic discovery, built-in Swagger UI\n- **aide**: More flexible, supports multiple doc formats\n\n### vs. paperclip\n\n- **Vespera**: Axum-first, modern OpenAPI 3.1\n- **paperclip**: Actix-focused, OpenAPI 2.0/3.0\n\n---\n\n## License\n\nApache-2.0\n\n---\n\n## Acknowledgments\n\nInspired by [FastAPI](https://fastapi.tiangolo.com/)'s developer experience and [Next.js](https://nextjs.org/)'s file-based routing.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdev-five-git%2Fvespera","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdev-five-git%2Fvespera","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdev-five-git%2Fvespera/lists"}