OpenAPI Schema Generation
mik-sdk automatically generates OpenAPI 3.0 schemas from your routes! macro, derive macros, and doc comments. The schema is generated at test time only and is never included in the WASM binary.
Quick Start
Section titled “Quick Start”-
Define routes with typed inputs and outputs:
#[derive(Type)]pub struct User {pub id: String,pub name: String,}/// List all usersroutes! {GET "/users" => list_users -> Vec<User>,} -
Generate the schema:
Terminal window cargo test __mik_write_schema -- --nocapture -
Find your schema at
openapi.jsonin the crate root.
How It Works
Section titled “How It Works”The routes! macro generates a hidden __mik_schema module containing:
json()- Returns the complete OpenAPI schema as JSONwrite_to(path)- Writes the schema to a file
A test named __mik_write_schema is also generated that writes the schema to openapi.json.
// Auto-generated by routes! macro (simplified)#[cfg(not(target_arch = "wasm32"))]pub mod __mik_schema { pub fn json() -> &'static str { /* OpenAPI JSON */ } pub fn write_to(path: &Path) -> io::Result<()> { /* write file */ }}
#[cfg(all(not(target_arch = "wasm32"), test))]#[test]fn __mik_write_schema() { __mik_schema::write_to(Path::new("openapi.json")).unwrap();}Generating Schemas
Section titled “Generating Schemas”Single Crate
Section titled “Single Crate”# Navigate to your handler cratecd examples/hello-world
# Generate openapi.jsoncargo test __mik_write_schema -- --nocapture
# Output:# Generated openapi.json (4119 bytes)The schema is written to openapi.json in the crate’s root directory (where Cargo.toml is located).
Workspace-Wide
Section titled “Workspace-Wide”Generate schemas for all crates that use the routes! macro:
# From workspace rootcargo test __mik_write_schema --workspace -- --nocaptureEach crate with a routes! macro will have its own openapi.json generated.
Verify the Schema
Section titled “Verify the Schema”# Pretty-print with jqcat openapi.json | jq .
# Validate with an OpenAPI linternpx @redocly/cli lint openapi.jsonWhat’s Included in the Schema
Section titled “What’s Included in the Schema”The generated schema includes:
| Source | Schema Content |
|---|---|
Cargo.toml | API title, version, and description (from package.name, package.version, package.description) |
| Default | Servers array with WASI P2 runtime description |
| Handler functions | Unique operationId (format: package_name.handler_name) |
routes! paths | Path definitions with methods |
#[derive(Type)] | Request/response body schemas |
#[derive(Query)] | Query parameter definitions |
#[derive(Path)] | Path parameter definitions |
/// doc comments | Operation summaries |
#[field(x_* = ...)] | OpenAPI extension attributes |
#[field(deprecated = true)] | Deprecated field markers |
#[deprecated] on routes | Deprecated operation markers |
#[status(code)] on routes | Custom success status codes (201, 204, etc.) |
| Path prefixes | Auto-generated tags (e.g., /users/{id} → Users) |
| Error responses | RFC 7807 ProblemDetails for 4XX/5XX |
API Metadata from Cargo.toml
Section titled “API Metadata from Cargo.toml”The schema’s info section is automatically populated from your Cargo.toml:
[package]name = "my-api"version = "0.1.0"description = "My awesome API service"Generates:
{ "info": { "title": "my-api", "version": "0.1.0", "description": "My awesome API service" }}Servers Array
Section titled “Servers Array”The schema includes a default server entry indicating the portable nature of WASI components:
{ "servers": [ { "url": "/", "description": "WASI HTTP component - runs on any WASI Preview 2 compliant runtime" } ]}The relative URL / means the API is served from wherever the component is deployed, making it truly portable across different hosts and runtimes.
Operation IDs
Section titled “Operation IDs”Each route automatically gets a unique operationId in the format package_name.handler_name. This ensures uniqueness when merging multiple OpenAPI schemas from different services.
// In crate "user-service"routes! { GET "/users" => list_users, POST "/users" => create_user,}Generates:
operationId: "user_service.list_users"operationId: "user_service.create_user"
Example Output
Section titled “Example Output”Given this handler:
/// User resource#[derive(Type)]pub struct User { pub id: String, #[field(min = 1, max = 100)] pub name: String, #[field(format = "email")] pub email: Option<String>,}
#[derive(Query)]pub struct ListQuery { pub search: Option<String>, #[field(default = 1)] pub page: u32, #[field(default = 20, max = 100)] pub limit: u32,}
/// List all users with paginationroutes! { GET "/users" => list_users(query: ListQuery) -> Vec<User>,}The generated schema includes:
{ "openapi": "3.0.0", "info": { "title": "my-api", "version": "0.1.0", "description": "My API service" }, "paths": { "/users": { "get": { "operationId": "my_api.list_users", "tags": ["Users"], "summary": "List all users with pagination", "parameters": [ { "name": "search", "in": "query", "required": false, "schema": { "type": "string" } }, { "name": "page", "in": "query", "required": false, "schema": { "type": "integer", "default": 1 } }, { "name": "limit", "in": "query", "required": false, "schema": { "type": "integer", "default": 20, "maximum": 100 } } ], "responses": { "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/User" } } } } }, "4XX": { "description": "Client Error", "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } }, "5XX": { "description": "Server Error", "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } } } } } }, "components": { "schemas": { "User": { "type": "object", "required": ["id", "name"], "properties": { "id": { "type": "string" }, "name": { "type": "string", "minLength": 1, "maxLength": 100 }, "email": { "type": "string", "format": "email", "nullable": true } } }, "ProblemDetails": { "type": "object", "description": "RFC 7807 Problem Details for HTTP APIs", "required": ["type", "title", "status"], "properties": { "type": { "type": "string", "default": "about:blank" }, "title": { "type": "string" }, "status": { "type": "integer", "minimum": 100, "maximum": 599 }, "detail": { "type": "string" }, "instance": { "type": "string" } } } } }}Adding Documentation
Section titled “Adding Documentation”Operation Summaries
Section titled “Operation Summaries”Add doc comments directly above routes:
routes! { /// List all users with optional search GET "/users" => list_users(query: ListQuery),
/// Create a new user POST "/users" => create_user(body: CreateInput),
/// Get user by ID GET "/users/{id}" => get_user(path: Id),}Field Descriptions
Section titled “Field Descriptions”Use the docs attribute on fields:
#[derive(Type)]pub struct CreateUserInput { #[field(docs = "User's display name", min = 1, max = 100)] pub name: String,
#[field(docs = "Valid email address", format = "email")] pub email: String,
#[field(docs = "Optional profile bio")] pub bio: Option<String>,}OpenAPI Extension Attributes (x-attrs)
Section titled “OpenAPI Extension Attributes (x-attrs)”Add custom x-* extension attributes to fields using the x_ prefix. Underscores are converted to hyphens in the output:
#[derive(Type)]pub struct User { #[field(x_example = "john@example.com", x_sensitive = true)] pub email: String,
#[field(x_deprecated_reason = "Use user_id instead", x_internal = true)] pub id: String,
#[field(x_priority = 10)] pub name: String,}Generates:
{ "email": { "type": "string", "x-example": "john@example.com", "x-sensitive": true }, "id": { "type": "string", "x-deprecated-reason": "Use user_id instead", "x-internal": true }, "name": { "type": "string", "x-priority": 10 }}Supported value types:
- String:
x_example = "value" - Boolean:
x_internal = true - Integer:
x_priority = 10 - Float:
x_weight = 0.5
Deprecated Fields and Routes
Section titled “Deprecated Fields and Routes”Mark fields or routes as deprecated using the deprecated attribute:
#[derive(Type)]pub struct User { pub id: String,
#[field(deprecated = true, docs = "Use id instead")] pub legacy_id: String,}
routes! { GET "/users" => list_users,
#[deprecated] GET "/api/v1/users" => list_users_v1,}Generates:
{ "properties": { "id": { "type": "string" }, "legacy_id": { "type": "string", "deprecated": true, "description": "Use id instead" } }}For routes:
{ "/api/v1/users": { "get": { "deprecated": true, "summary": "..." } }}Custom Status Codes
Section titled “Custom Status Codes”By default, all routes document a 200 success response. Use #[status(code)] to specify a different status code:
routes! { GET "/users" => list_users -> Vec<User>, // 200 (default)
#[status(201)] POST "/users" => create_user(body: Input) -> User, // 201 Created
#[status(202)] POST "/jobs" => start_job(body: JobInput) -> Job, // 202 Accepted
#[status(204)] DELETE "/users/{id}" => delete_user(path: Id), // 204 No Content}Generates appropriate response documentation:
{ "/users": { "post": { "responses": { "201": { "description": "Created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/User" } } } } } } }}Common status codes and their descriptions:
| Code | Description | Use Case |
|---|---|---|
200 | Success | Default for GET, PUT, PATCH |
201 | Created | POST that creates a resource |
202 | Accepted | Async operations |
204 | No Content | DELETE, or updates with no body |
CI Integration
Section titled “CI Integration”Add schema generation to your CI pipeline:
- name: Generate OpenAPI schemas run: cargo test __mik_write_schema --workspace -- --nocapture
- name: Validate OpenAPI schemas run: | npm install -g @redocly/cli find . -name "openapi.json" -exec redocly lint {} \;
- name: Upload schemas as artifacts uses: actions/upload-artifact@v4 with: name: openapi-schemas path: "**/openapi.json"Using the Schema
Section titled “Using the Schema”Swagger UI / Redoc
Section titled “Swagger UI / Redoc”Serve your openapi.json with documentation tools:
# Swagger UIdocker run -p 8080:8080 -e SWAGGER_JSON=/schema/openapi.json \ -v $(pwd)/openapi.json:/schema/openapi.json swaggerapi/swagger-ui
# Redocnpx @redocly/cli preview-docs openapi.jsonClient Generation
Section titled “Client Generation”Generate typed clients from your schema:
# TypeScriptnpx openapi-typescript openapi.json -o api-types.ts
# Rustopenapi-generator generate -i openapi.json -g rust -o ./client
# Pythonopenapi-generator generate -i openapi.json -g python -o ./clientDesign Philosophy
Section titled “Design Philosophy”The schema generation uses utoipa internally for type-safe OpenAPI building, ensuring the generated schema is always valid OpenAPI 3.0.
Troubleshooting
Section titled “Troubleshooting”Schema Not Generated
Section titled “Schema Not Generated”Ensure your crate has the routes! macro:
routes! { GET "/" => home,}Missing Types in Schema
Section titled “Missing Types in Schema”Types must use derive macros to appear in the schema:
#[derive(Type)] // For request/response bodies#[derive(Query)] // For query parameters#[derive(Path)] // For path parametersTest Not Found
Section titled “Test Not Found”The test is only generated when NOT targeting WASM:
# Correct - native targetcargo test __mik_write_schema
# Wrong - WASM target (test won't exist)cargo test --target wasm32-wasip2 __mik_write_schemaNext Steps
Section titled “Next Steps”- Routing - Define routes with typed inputs
- Request Reference - Access request data
- Response Reference - Response macros