tideway 0.7.24

A batteries-included Rust web framework built on Axum for building SaaS applications quickly
Documentation
# OpenAPI Documentation

Tideway provides built-in OpenAPI 3.0 documentation support using the `utoipa` crate. This allows you to generate type-safe API documentation and client SDKs automatically.

## Features

- **Automatic documentation generation** from code annotations
- **Swagger UI** for interactive API exploration
- **OpenAPI 3.0 specification** endpoint for client generation
- **Flexible visibility control** for different environments
- **JWT Bearer authentication** support

## Configuration

OpenAPI support is controlled via environment variables:

### Environment Variables

```bash
# Framework defaults: false for all three values below.
# The API preset scaffold writes these as `true` in `.env.example` for local development.
OPENAPI_ENABLED=true

# Enable/disable Swagger UI
OPENAPI_SWAGGER_UI=true

# Serve OpenAPI spec JSON endpoint
OPENAPI_SERVE_SPEC=true

# Control visibility of endpoints (default: all)
# Options: all, public, internal
OPENAPI_VISIBILITY=all
```

### Custom Paths

You can customize the Swagger UI and spec paths:

```bash
# Swagger UI path (default: /swagger-ui)
OPENAPI_SWAGGER_UI_PATH=/swagger-ui

# OpenAPI spec path (default: /api-docs/openapi.json)
OPENAPI_SPEC_PATH=/api-docs/openapi.json
```

## Accessing the Documentation

Once your application is running with OpenAPI enabled:

1. **Swagger UI**: Visit `http://localhost:8000/swagger-ui` to explore the API interactively
2. **OpenAPI Spec**: Download the spec from `http://localhost:8000/api-docs/openapi.json`

The API preset merges its generated resource and authentication documents. Authentication operations declare JWT bearer security where required. Sensitive request fields such as passwords, refresh tokens, MFA challenge tokens, and verification codes are marked `writeOnly`.

The scaffold enables Swagger UI and the JSON specification for local development. Treat both as deployment choices: keep them disabled for a private production API unless they are intentionally exposed and protected by your edge or application access controls.

## Usage Scenarios

### Development - Private API

For private APIs where you only need type generation locally:

```bash
OPENAPI_ENABLED=true
OPENAPI_SWAGGER_UI=false
OPENAPI_SERVE_SPEC=false
```

This allows you to generate the OpenAPI spec during development without exposing it in production.

### Production - Public API

For public APIs where you want full documentation:

```bash
OPENAPI_ENABLED=true
OPENAPI_SWAGGER_UI=true
OPENAPI_SERVE_SPEC=true
OPENAPI_VISIBILITY=public
```

### Mixed - Public and Internal Endpoints

For APIs with both public and internal endpoints:

```bash
OPENAPI_ENABLED=true
OPENAPI_SWAGGER_UI=true
OPENAPI_SERVE_SPEC=true
OPENAPI_VISIBILITY=public  # Only show public endpoints
```

Tag your internal endpoints with `tag = "internal"` and public ones with appropriate tags like `"customers"`, `"orders"`, etc.

## Splitting OpenAPI Docs by Module

For large applications, you can derive `OpenApi` per module and merge them:

```rust
#[derive(utoipa::OpenApi)]
#[openapi(paths(crate::routes::users::list_users))]
struct UsersDoc;

#[derive(utoipa::OpenApi)]
#[openapi(paths(crate::routes::billing::get_invoices))]
struct BillingDoc;

let openapi = tideway::openapi_merge!(UsersDoc, BillingDoc);
```

If you want less boilerplate, use `openapi_doc!`:

```rust
tideway::openapi_doc!(pub(crate) UsersDoc, paths(crate::routes::users::list_users));
tideway::openapi_doc!(pub(crate) BillingDoc, paths(crate::routes::billing::get_invoices));

let openapi = tideway::openapi_merge!(UsersDoc, BillingDoc);
```

For large schema lists, use `openapi_components!`:

```rust
tideway::openapi_components!(
    pub(crate) ComponentsDoc,
    schemas(crate::routes::users::UserResponse, crate::routes::billing::InvoiceResponse)
);
```

You can also add modifiers:

```rust
tideway::openapi_components!(
    pub(crate) ComponentsDoc,
    schemas(crate::routes::users::UserResponse),
    modifiers(&SecurityAddon)
);
```

If you already have `OpenApi` values, you can merge them directly:

```rust
let openapi = tideway::openapi::merge_openapi(vec![doc_a, doc_b]);
```

To reduce repetition, you can merge docs from the same module with:

```rust
let openapi = tideway::openapi_merge_module!(openapi_docs, UsersDoc, BillingDoc);
```

## Adding Documentation to Your Endpoints

### 1. Add Schema Derives to Your Types

```rust
use serde::{Deserialize, Serialize};

#[derive(Debug, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct CreateCustomerRequest {
    pub name: String,
    pub email: Option<String>,
}

#[derive(Debug, Serialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct CustomerResponse {
    pub id: i64,
    pub name: String,
    pub email: Option<String>,
}
```

**Note**: Use `utoipa::IntoParams` for query parameters:

```rust
#[derive(Debug, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::IntoParams))]
pub struct PaginationParams {
    pub page: u64,
    pub per_page: u64,
}
```

### 2. Annotate Your Handler Functions

**Recommended: Using the `#[api]` macro** (requires `macros` feature):

```rust
use tideway::api;

/// Create a new customer
#[api(post, "/api/customers", tag = "customers")]
async fn create_customer(
    AuthUser(user): AuthUser<MyAuthProvider>,
    Json(req): Json<CreateCustomerRequest>,
) -> Result<Json<ApiResponse<CustomerResponse>>> {
    // Implementation
}

/// Get a customer by ID
#[api(get, "/api/customers/:id", tag = "customers")]
async fn get_customer(
    AuthUser(user): AuthUser<MyAuthProvider>,
    Path(id): Path<i64>,
) -> Result<Json<ApiResponse<CustomerResponse>>> {
    // Implementation
}
```

The `#[api]` macro automatically infers:
- Path parameters from `:param` syntax (converted to `{param}` for OpenAPI)
- Request body from `Json<T>` extractors
- Query parameters from `Query<T>` extractors (if T implements `IntoParams`)
- Security requirement from `AuthUser<T>` extractors
- Response type from return type

**Override options:**
- `security = "none"` - disable authentication requirement for public endpoints
- `tag = "custom_tag"` - override the default tag

**Alternative: Manual utoipa annotations** (for edge cases):

```rust
/// Create a new customer
#[cfg_attr(feature = "openapi", utoipa::path(
    post,
    path = "/customers",
    tag = "customers",
    request_body = CreateCustomerRequest,
    responses(
        (status = 200, description = "Customer created", body = ApiResponse<CustomerResponse>),
        (status = 401, description = "Unauthorized"),
        (status = 500, description = "Internal server error")
    ),
    security(("bearer_auth" = []))
))]
async fn create_customer(
    Json(req): Json<CreateCustomerRequest>,
) -> Result<Json<ApiResponse<CustomerResponse>>> {
    // Implementation
}
```

Use manual annotations for:
- Handlers returning `()` or `impl IntoResponse`
- Handlers returning `StatusCode` or `Response` directly
- Query types that don't implement `IntoParams`

### 3. Register Paths in main.rs

Add your endpoints to the `ApiDoc` struct:

```rust
#[cfg(feature = "openapi")]
#[derive(OpenApi)]
#[openapi(
    paths(
        // Add your handlers here
        sh_api::routes::customers::create_customer,
        sh_api::routes::customers::list_customers,
    ),
    components(
        schemas(
            // Add your request/response types here
            sh_api::routes::customers::CreateCustomerRequest,
            sh_api::routes::customers::CustomerResponse,
        )
    ),
    tags(
        (name = "customers", description = "Customer management endpoints")
    ),
    modifiers(&SecurityAddon)
)]
struct ApiDoc;
```

## Generating Client SDKs

You can use the OpenAPI spec to generate type-safe client libraries:

### TypeScript/Vue

```bash
# Install openapi-typescript
npm install -D openapi-typescript

# Generate TypeScript types
npx openapi-typescript http://localhost:8080/api-docs/openapi.json -o src/api/types.ts
```

### Other Languages

Use [openapi-generator](https://openapi-generator.tech/):

```bash
# Install openapi-generator-cli
npm install -g @openapitools/openapi-generator-cli

# Generate Python client
openapi-generator-cli generate \
  -i http://localhost:8080/api-docs/openapi.json \
  -g python \
  -o ./client/python

# Generate Go client
openapi-generator-cli generate \
  -i http://localhost:8080/api-docs/openapi.json \
  -g go \
  -o ./client/go
```

## Best Practices

1. **Always use `cfg_attr`**: Wrap OpenAPI derives and macros with `#[cfg_attr(feature = "openapi", ...)]` to keep them optional
2. **Document your endpoints**: Use the doc comments (`///`) - they appear in Swagger UI
3. **Use appropriate tags**: Group related endpoints with meaningful tags
4. **Specify all response codes**: Document success and error responses
5. **Keep schemas simple**: Complex nested generics can be difficult for OpenAPI to represent

## Disabling OpenAPI

To completely disable OpenAPI support, either:

1. Remove the `openapi` feature from `default` features in `Cargo.toml`
2. Set `OPENAPI_ENABLED=false` in your environment

## Troubleshooting

### Swagger UI not appearing

- Check that `OPENAPI_ENABLED=true` and `OPENAPI_SWAGGER_UI=true`
- Verify the path: default is `/swagger-ui`
- Check logs for "OpenAPI documentation enabled"

### Endpoints missing from documentation

- Ensure the handler function has `#[cfg_attr(feature = "openapi", utoipa::path(...))]`
- Verify the path is registered in the `ApiDoc` struct in `main.rs`
- Check that request/response types have `ToSchema` or `IntoParams` derives

### Generic type errors

For complex generic types like `ApiResponse<PaginatedResponse<CustomerResponse>>`, you may need to explicitly list them in the `components.schemas` section.

## Resources

- [utoipa documentation]https://docs.rs/utoipa/latest/utoipa/
- [utoipa-swagger-ui documentation]https://docs.rs/utoipa-swagger-ui/latest/utoipa_swagger_ui/
- [OpenAPI Specification]https://spec.openapis.org/oas/latest.html
- [Swagger UI]https://swagger.io/tools/swagger-ui/