# 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`
## 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/)