Skip to main content

Overview

TrailBase automatically generates OpenAPI 3.0 specifications for your APIs, providing interactive documentation through Swagger UI and enabling API client generation.

Accessing API Documentation

Swagger UI

Access interactive API documentation at:
https://yourdomain.com/_/docs
The Swagger UI provides:
  • Interactive testing: Try API endpoints directly from the browser
  • Request/response examples: See sample data for each endpoint
  • Authentication: Test authenticated endpoints
  • Schema documentation: Explore data models and types

OpenAPI JSON

Download the raw OpenAPI specification:
curl https://yourdomain.com/openapi.json > api-spec.json

API Structure

From crates/core/src/lib.rs:
use utoipa::OpenApi;

#[derive(OpenApi)]
#[openapi(
    info(
        title = "TrailBase",
        description = "TrailBase APIs",
    ),
    nest(
        (path = "/api/auth/v1", api = crate::auth::AuthApi),
        (path = "/api/records/v1", api = crate::records::RecordOpenApi),
    ),
    tags(),
)]
pub struct Doc;

API Categories

Authentication API

Endpoints under /api/auth/v1:
  • POST /register - Register new user
  • POST /login - Login with email/password
  • POST /logout - Logout current user
  • GET /user - Get current user profile
  • POST /verify_email - Request email verification
  • POST /reset_password - Request password reset
  • GET /oauth/{provider} - OAuth login
  • GET /oauth/{provider}/callback - OAuth callback
  • GET /oauth/providers - List OAuth providers

Records API

Endpoints under /api/records/v1/{table}:
  • GET /{table} - List records
  • POST /{table} - Create record
  • GET /{table}/{id} - Get record by ID
  • PATCH /{table}/{id} - Update record
  • DELETE /{table}/{id} - Delete record
  • GET /files/{id} - Download file

Custom API Documentation

Document Custom Endpoints

Add OpenAPI annotations to your endpoints:
use utoipa::{OpenApi, IntoParams, ToSchema};

#[derive(Debug, Deserialize, IntoParams)]
struct SearchQuery {
    /// Search query string
    #[param(example = "rust")]
    q: String,
    
    /// Maximum number of results
    #[param(minimum = 1, maximum = 100, default = 10)]
    limit: Option<i32>,
}

#[derive(Debug, Serialize, ToSchema)]
struct SearchResult {
    /// Unique identifier
    id: i64,
    
    /// Result title
    title: String,
    
    /// Relevance score (0-1)
    #[schema(minimum = 0, maximum = 1)]
    score: f64,
}

/// Search for items
#[utoipa::path(
    get,
    path = "/search",
    params(SearchQuery),
    responses(
        (status = 200, description = "Search results", body = Vec<SearchResult>),
        (status = 400, description = "Invalid query"),
    ),
    tag = "search"
)]
async fn search(Query(params): Query<SearchQuery>) -> Json<Vec<SearchResult>> {
    // Implementation
}

Group Custom APIs

#[derive(OpenApi)]
#[openapi(
    paths(
        search,
        get_recommendations,
        get_trending,
    ),
    components(
        schemas(SearchResult, Recommendation, TrendingItem)
    ),
    tags(
        (name = "search", description = "Search operations"),
        (name = "discovery", description = "Content discovery")
    )
)]
struct CustomApi;

Schema Generation

Derive Schema

use utoipa::ToSchema;
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize, ToSchema)]
#[schema(example = json!({
    "id": 1,
    "title": "Example Post",
    "published": true
}))]
struct Post {
    /// Unique post identifier
    #[schema(minimum = 1)]
    id: i64,
    
    /// Post title
    #[schema(min_length = 1, max_length = 200)]
    title: String,
    
    /// Post content in Markdown
    content: String,
    
    /// Whether the post is published
    #[schema(default = false)]
    published: bool,
    
    /// Publication date
    #[schema(value_type = String, format = "date-time")]
    published_at: Option<chrono::DateTime<chrono::Utc>>,
    
    /// Post tags
    #[schema(example = json!(["rust", "api"]))]
    tags: Vec<String>,
}

#[derive(Debug, Serialize, Deserialize, ToSchema)]
enum PostStatus {
    /// Post is in draft state
    Draft,
    /// Post is published and visible
    Published,
    /// Post is archived
    Archived,
}

Nested Schemas

#[derive(Debug, Serialize, ToSchema)]
struct Author {
    id: i64,
    name: String,
    email: String,
}

#[derive(Debug, Serialize, ToSchema)]
struct PostWithAuthor {
    #[serde(flatten)]
    post: Post,
    
    /// Post author
    author: Author,
}

Response Documentation

Success Responses

#[utoipa::path(
    get,
    path = "/posts/{id}",
    responses(
        (status = 200, description = "Post found", body = Post),
        (status = 404, description = "Post not found"),
        (status = 401, description = "Unauthorized"),
    ),
    params(
        ("id" = i64, Path, description = "Post ID")
    )
)]
async fn get_post(Path(id): Path<i64>) -> Result<Json<Post>, StatusCode> {
    // Implementation
}

Error Responses

#[derive(Debug, Serialize, ToSchema)]
struct ErrorResponse {
    /// Error message
    error: String,
    
    /// Optional error details
    details: Option<serde_json::Value>,
}

#[utoipa::path(
    post,
    path = "/posts",
    request_body = Post,
    responses(
        (status = 201, description = "Post created", body = Post),
        (status = 400, description = "Invalid input", body = ErrorResponse),
        (status = 401, description = "Unauthorized"),
    )
)]
async fn create_post(Json(post): Json<Post>) -> Result<Json<Post>, (StatusCode, Json<ErrorResponse>)> {
    // Implementation
}

Authentication Documentation

Bearer Token

use utoipa::openapi::security::{HttpAuthScheme, HttpBuilder, SecurityScheme};

#[derive(OpenApi)]
#[openapi(
    modifiers(&SecurityAddon),
)]
struct ApiDoc;

struct SecurityAddon;

impl utoipa::Modify for SecurityAddon {
    fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
        if let Some(components) = openapi.components.as_mut() {
            components.add_security_scheme(
                "bearer_auth",
                SecurityScheme::Http(
                    HttpBuilder::new()
                        .scheme(HttpAuthScheme::Bearer)
                        .bearer_format("JWT")
                        .build()
                ),
            )
        }
    }
}

#[utoipa::path(
    get,
    path = "/protected",
    responses(
        (status = 200, description = "Success"),
    ),
    security(
        ("bearer_auth" = [])
    )
)]
async fn protected_endpoint() -> String {
    "Protected data".to_string()
}

Client Generation

Generate TypeScript Client

Use openapi-typescript:
# Install
npm install -D openapi-typescript

# Generate types
npx openapi-typescript https://yourdomain.com/openapi.json -o ./src/api-types.ts
Usage:
import type { paths } from './api-types';

type SearchParams = paths['/api/search']['get']['parameters']['query'];
type SearchResponse = paths['/api/search']['get']['responses']['200']['content']['application/json'];

async function search(params: SearchParams): Promise<SearchResponse> {
  const query = new URLSearchParams(params as any);
  const response = await fetch(`/api/search?${query}`);
  return response.json();
}

Generate Python Client

Use openapi-python-client:
pip install openapi-python-client

openapi-python-client generate --url https://yourdomain.com/openapi.json

Generate Go Client

Use oapi-codegen:
go install github.com/deepmap/oapi-codegen/cmd/oapi-codegen@latest

oapi-codegen -package api https://yourdomain.com/openapi.json > api/client.go

Testing with OpenAPI

Validate Responses

#[cfg(test)]
mod tests {
    use super::*;
    
    #[tokio::test]
    async fn test_openapi_spec() {
        let spec = ApiDoc::openapi();
        
        // Validate spec is valid OpenAPI
        assert!(spec.info.title == "TrailBase");
        assert!(!spec.paths.paths.is_empty());
        
        // Check all endpoints have responses
        for (path, item) in spec.paths.paths.iter() {
            if let Some(operation) = &item.get {
                assert!(
                    !operation.responses.responses.is_empty(),
                    "GET {} missing responses",
                    path
                );
            }
        }
    }
}

Contract Testing

import { test, expect } from '@jest/globals';
import OpenAPIValidator from 'openapi-validator';

test('API responses match OpenAPI spec', async () => {
  const spec = await fetch('https://api.example.com/openapi.json').then(r => r.json());
  const validator = new OpenAPIValidator(spec);
  
  const response = await fetch('https://api.example.com/api/records/v1/posts');
  const body = await response.json();
  
  const validation = validator.validate('/api/records/v1/posts', 'get', response.status, body);
  expect(validation.errors).toHaveLength(0);
});

Best Practices

1

Document all endpoints

Add OpenAPI annotations to every public endpoint for complete documentation.
2

Provide examples

Include example requests and responses:
#[schema(example = json!({
    "id": 1,
    "name": "Example"
}))]
3

Use descriptive tags

Group related endpoints with tags:
#[utoipa::path(
    tag = "users",
    // ...
)]
4

Document errors

Specify all possible error responses with status codes and error schemas.
5

Keep specs in sync

Run validation tests to ensure OpenAPI specs match implementation.
6

Version your APIs

Use path prefixes for API versioning:
nest(
    (path = "/api/v1", api = ApiV1),
    (path = "/api/v2", api = ApiV2),
)

Customizing Swagger UI

The Swagger UI can be customized by modifying the HTML template in the TrailBase source:
<!DOCTYPE html>
<html>
<head>
  <title>API Documentation</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css">
  <style>
    /* Custom styles */
    .swagger-ui .topbar { display: none; }
  </style>
</head>
<body>
  <div id="swagger-ui"></div>
  <script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
  <script>
    SwaggerUIBundle({
      url: '/openapi.json',
      dom_id: '#swagger-ui',
      deepLinking: true,
      presets: [
        SwaggerUIBundle.presets.apis,
        SwaggerUIBundle.SwaggerUIStandalonePreset
      ],
    });
  </script>
</body>
</html>

Alternative API Documentation

Redoc

Use Redoc for a different documentation style:
<!DOCTYPE html>
<html>
<head>
  <title>API Reference</title>
  <script src="https://cdn.jsdelivr.net/npm/redoc@latest/bundles/redoc.standalone.js"></script>
</head>
<body>
  <redoc spec-url="/openapi.json"></redoc>
</body>
</html>

RapiDoc

RapiDoc offers a customizable API console:
<!DOCTYPE html>
<html>
<head>
  <script type="module" src="https://unpkg.com/rapidoc/dist/rapidoc-min.js"></script>
</head>
<body>
  <rapi-doc
    spec-url="/openapi.json"
    theme="dark"
    render-style="view"
  ></rapi-doc>
</body>
</html>

Next Steps

Custom Endpoints

Build documented APIs

WASM Components

Create custom endpoints

OAuth Providers

Document auth flows

Object Storage

File upload APIs

Build docs developers (and LLMs) love