OpenAPI

Working with generated specs

Humus automatically generates OpenAPI 3.0 specifications for all REST APIs, providing comprehensive API documentation with zero manual effort.

Overview

Every Humus REST API includes:

  • Automatic OpenAPI 3.0 schema generation from Go types
  • Live specification endpoint at GET /openapi.json
  • Request/response schemas via reflection
  • Parameter validation rules in the specification
  • Security scheme documentation for authentication
  • No manual annotation required - schemas generated from code

This enables seamless integration with tools like Swagger UI, Postman, ReDoc, and API client generators.

Accessing the OpenAPI Specification

Every API created with rest.NewApi() automatically serves its OpenAPI specification:

# Get the full OpenAPI spec
curl http://localhost:8080/openapi.json

# Pretty-print with jq
curl http://localhost:8080/openapi.json | jq

Basic Example

package main

import (
    "context"
    "net/http"

    "github.com/z5labs/humus/rest"
)

type CreateUserRequest struct {
    Email    string `json:"email"`
    Name     string `json:"name"`
    Age      int    `json:"age,omitempty"`
}

type UserResponse struct {
    ID       string `json:"id"`
    Email    string `json:"email"`
    Name     string `json:"name"`
    Age      int    `json:"age,omitempty"`
    Created  string `json:"created"`
}

func Init(ctx context.Context, cfg rest.Config) (*rest.Api, error) {
    handler := rest.HandlerFunc[CreateUserRequest, UserResponse](
        func(ctx context.Context, req *CreateUserRequest) (*UserResponse, error) {
            // Implementation
            return &UserResponse{
                ID:      "user-123",
                Email:   req.Email,
                Name:    req.Name,
                Age:     req.Age,
                Created: "2025-01-15T10:30:00Z",
            }, nil
        },
    )

    api := rest.NewApi(
        "User Management API",
        "1.0.0",
        rest.Handle(
            http.MethodPost,
            rest.BasePath("/users"),
            rest.HandleJson(handler),
        ),
    )

    return api, nil
}

Generated OpenAPI Schema:

{
  "openapi": "3.0",
  "info": {
    "title": "User Management API",
    "version": "1.0.0"
  },
  "paths": {
    "/users": {
      "post": {
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {"type": "string"},
                  "name": {"type": "string"},
                  "age": {"type": "integer"}
                },
                "required": ["email", "name"]
              }
            }
          }
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {"type": "string"},
                    "email": {"type": "string"},
                    "name": {"type": "string"},
                    "age": {"type": "integer"},
                    "created": {"type": "string"}
                  },
                  "required": ["id", "email", "name", "created"]
                }
              }
            }
          }
        }
      }
    }
  }
}

Schema Generation

Humus uses reflection via github.com/swaggest/jsonschema-go to automatically generate JSON schemas from Go types.

Basic Types

Go types map to JSON schema types:

type Product struct {
    Name        string  `json:"name"`           // "type": "string"
    Price       float64 `json:"price"`          // "type": "number"
    Quantity    int     `json:"quantity"`       // "type": "integer"
    Available   bool    `json:"available"`      // "type": "boolean"
    Tags        []string `json:"tags"`          // "type": "array", "items": {"type": "string"}
}

Optional Fields

Use omitempty to mark fields as optional:

type User struct {
    ID    string `json:"id"`              // Required
    Email string `json:"email"`           // Required
    Phone string `json:"phone,omitempty"` // Optional
}

Generated Schema:

{
  "type": "object",
  "properties": {
    "id": {"type": "string"},
    "email": {"type": "string"},
    "phone": {"type": "string"}
  },
  "required": ["id", "email"]
}

Nested Objects

Nested structs are automatically expanded:

type Address struct {
    Street  string `json:"street"`
    City    string `json:"city"`
    Country string `json:"country"`
}

type Customer struct {
    Name    string  `json:"name"`
    Address Address `json:"address"`
}

Generated Schema:

{
  "type": "object",
  "properties": {
    "name": {"type": "string"},
    "address": {
      "type": "object",
      "properties": {
        "street": {"type": "string"},
        "city": {"type": "string"},
        "country": {"type": "string"}
      },
      "required": ["street", "city", "country"]
    }
  },
  "required": ["name", "address"]
}

Arrays and Slices

type Catalog struct {
    Products  []Product           `json:"products"`
    Categories []string           `json:"categories"`
    Tags      map[string]string  `json:"tags"`
}

Generated Schema:

{
  "type": "object",
  "properties": {
    "products": {
      "type": "array",
      "items": {
        "$ref": "#/components/schemas/Product"
      }
    },
    "categories": {
      "type": "array",
      "items": {"type": "string"}
    },
    "tags": {
      "type": "object",
      "additionalProperties": {"type": "string"}
    }
  }
}

Pointers

Pointer types become optional in the schema:

type UpdateRequest struct {
    Name  *string `json:"name,omitempty"`   // Optional, can be null
    Email *string `json:"email,omitempty"`  // Optional, can be null
}

Enumerations

Use type aliases or constants for enumerations:

type Status string

const (
    StatusPending   Status = "pending"
    StatusActive    Status = "active"
    StatusCompleted Status = "completed"
)

type Order struct {
    ID     string `json:"id"`
    Status Status `json:"status"`
}

Note: Basic Go enums don’t automatically generate enum constraints in the schema. For strict validation, implement custom schema methods or use validation in your handler.

Time and Date

Use time.Time for timestamps:

import "time"

type Event struct {
    Name      string    `json:"name"`
    StartTime time.Time `json:"start_time"`
    EndTime   time.Time `json:"end_time,omitempty"`
}

Generated Schema:

{
  "type": "object",
  "properties": {
    "name": {"type": "string"},
    "start_time": {"type": "string", "format": "date-time"},
    "end_time": {"type": "string", "format": "date-time"}
  },
  "required": ["name", "start_time"]
}

Path Parameters

Path parameters are automatically included in the OpenAPI spec:

rest.Handle(
    http.MethodGet,
    rest.BasePath("/users").Param("id").Path("orders").Param("orderId"),
    getOrderHandler,
)

Generated Operation:

{
  "paths": {
    "/users/{id}/orders/{orderId}": {
      "get": {
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {"type": "string"}
          },
          {
            "name": "orderId",
            "in": "path",
            "required": true,
            "schema": {"type": "string"}
          }
        ]
      }
    }
  }
}

Query Parameters

Query parameters defined with rest.QueryParam() appear in the spec:

rest.Handle(
    http.MethodGet,
    rest.BasePath("/search"),
    searchHandler,
    rest.QueryParam("q", rest.Required()),
    rest.QueryParam("limit", rest.Regex(regexp.MustCompile(`^\d+$`))),
    rest.QueryParam("offset"),
)

Generated Parameters:

{
  "parameters": [
    {
      "name": "q",
      "in": "query",
      "required": true,
      "schema": {"type": "string"}
    },
    {
      "name": "limit",
      "in": "query",
      "required": false,
      "schema": {
        "type": "string",
        "pattern": "^\\d+$"
      }
    },
    {
      "name": "offset",
      "in": "query",
      "required": false,
      "schema": {"type": "string"}
    }
  ]
}

Headers

Header parameters are documented in the specification:

rest.Handle(
    http.MethodGet,
    rest.BasePath("/data"),
    handler,
    rest.Header("X-API-Key", rest.Required()),
    rest.Header("X-Request-ID"),
)

Generated Parameters:

{
  "parameters": [
    {
      "name": "X-API-Key",
      "in": "header",
      "required": true,
      "schema": {"type": "string"}
    },
    {
      "name": "X-Request-ID",
      "in": "header",
      "required": false,
      "schema": {"type": "string"}
    }
  ]
}

Security Schemes

Authentication schemes are automatically documented. See Authentication for details.

JWT Authentication

rest.Handle(
    http.MethodPost,
    rest.BasePath("/orders"),
    createOrderHandler,
    rest.Header("Authorization", rest.Required(), rest.JWTAuth("jwt", verifier)),
)

Generated Security Scheme:

{
  "components": {
    "securitySchemes": {
      "jwt": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT"
      }
    }
  },
  "paths": {
    "/orders": {
      "post": {
        "security": [{"jwt": []}]
      }
    }
  }
}

API Key Authentication

rest.Handle(
    http.MethodGet,
    rest.BasePath("/data"),
    handler,
    rest.Header("X-API-Key", rest.Required(), rest.APIKey("api-key")),
)

Generated Security Scheme:

{
  "components": {
    "securitySchemes": {
      "api-key": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key"
      }
    }
  },
  "paths": {
    "/data": {
      "get": {
        "security": [{"api-key": []}]
      }
    }
  }
}

Integration with Tools

Swagger UI

Serve Swagger UI to visualize your API:

import (
    "net/http"
    "github.com/z5labs/humus/rest"
)

func Init(ctx context.Context, cfg rest.Config) (*rest.Api, error) {
    api := rest.NewApi(
        "My API",
        "1.0.0",
        // Your operations...
    )

    // Serve Swagger UI (manual setup)
    http.Handle("/docs/", http.StripPrefix("/docs/", http.FileServer(http.Dir("./swagger-ui"))))

    return api, nil
}

Configure Swagger UI to point to /openapi.json:

<!-- swagger-ui/index.html -->
<!DOCTYPE html>
<html>
<head>
    <link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@latest/swagger-ui.css">
</head>
<body>
    <div id="swagger-ui"></div>
    <script src="https://unpkg.com/swagger-ui-dist@latest/swagger-ui-bundle.js"></script>
    <script>
        SwaggerUIBundle({
            url: "/openapi.json",
            dom_id: '#swagger-ui'
        });
    </script>
</body>
</html>

Access at: http://localhost:8080/docs/

Postman

Import the OpenAPI spec into Postman:

  1. Open Postman
  2. Click Import
  3. Select Link and enter http://localhost:8080/openapi.json
  4. Postman generates a complete API collection with all endpoints

ReDoc

Serve ReDoc for clean API documentation:

<!DOCTYPE html>
<html>
<head>
    <title>API Documentation</title>
</head>
<body>
    <redoc spec-url="/openapi.json"></redoc>
    <script src="https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js"></script>
</body>
</html>

OpenAPI Generator

Generate client libraries in any language:

# Generate TypeScript client
openapi-generator-cli generate \
    -i http://localhost:8080/openapi.json \
    -g typescript-fetch \
    -o ./generated/typescript-client

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

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

Complete Example

package main

import (
    "context"
    "net/http"
    "time"

    "github.com/z5labs/humus/rest"
)

// Request/Response Types
type CreateBookRequest struct {
    Title       string   `json:"title"`
    Author      string   `json:"author"`
    ISBN        string   `json:"isbn,omitempty"`
    PublishDate string   `json:"publish_date,omitempty"`
    Tags        []string `json:"tags,omitempty"`
}

type Book struct {
    ID          string    `json:"id"`
    Title       string    `json:"title"`
    Author      string    `json:"author"`
    ISBN        string    `json:"isbn,omitempty"`
    PublishDate string    `json:"publish_date,omitempty"`
    Tags        []string  `json:"tags,omitempty"`
    CreatedAt   time.Time `json:"created_at"`
}

type BookList struct {
    Books      []Book `json:"books"`
    TotalCount int    `json:"total_count"`
    Page       int    `json:"page"`
}

// Handlers
func createBookHandler(ctx context.Context, req *CreateBookRequest) (*Book, error) {
    return &Book{
        ID:          "book-123",
        Title:       req.Title,
        Author:      req.Author,
        ISBN:        req.ISBN,
        PublishDate: req.PublishDate,
        Tags:        req.Tags,
        CreatedAt:   time.Now(),
    }, nil
}

func listBooksHandler(ctx context.Context) (*BookList, error) {
    page := rest.QueryParamValue(ctx, "page")
    // Use page parameter...

    return &BookList{
        Books: []Book{
            {
                ID:        "book-1",
                Title:     "Go Programming",
                Author:    "John Doe",
                CreatedAt: time.Now(),
            },
        },
        TotalCount: 1,
        Page:       1,
    }, nil
}

func getBookHandler(ctx context.Context) (*Book, error) {
    bookID := rest.PathParamValue(ctx, "id")
    // Fetch book by ID...

    return &Book{
        ID:        bookID,
        Title:     "Go Programming",
        Author:    "John Doe",
        CreatedAt: time.Now(),
    }, nil
}

type Config struct {
    rest.Config `config:",squash"`
}

func Init(ctx context.Context, cfg Config) (*rest.Api, error) {
    api := rest.NewApi(
        "Bookstore API",
        "1.0.0",
        // Create a book
        rest.Handle(
            http.MethodPost,
            rest.BasePath("/books"),
            rest.HandleJson(rest.HandlerFunc[CreateBookRequest, Book](createBookHandler)),
        ),
        // List books with pagination
        rest.Handle(
            http.MethodGet,
            rest.BasePath("/books"),
            rest.ProduceJson(rest.ProducerFunc[BookList](listBooksHandler)),
            rest.QueryParam("page"),
            rest.QueryParam("limit"),
        ),
        // Get a specific book
        rest.Handle(
            http.MethodGet,
            rest.BasePath("/books").Param("id"),
            rest.ProduceJson(rest.ProducerFunc[Book](getBookHandler)),
        ),
    )

    return api, nil
}

func main() {
    rest.Run(rest.YamlSource("config.yaml"), Init)
}

Access the specification:

# Start the service
go run main.go

# Get the OpenAPI spec
curl http://localhost:8080/openapi.json | jq

# The spec includes:
# - All three endpoints (/books POST, GET, /books/{id} GET)
# - Request schemas (CreateBookRequest)
# - Response schemas (Book, BookList)
# - Path parameters ({id})
# - Query parameters (page, limit)
# - Proper HTTP methods and status codes

Best Practices

1. Use Descriptive Type Names

Type names appear in the OpenAPI schema:

// Good - clear and descriptive
type CreateUserRequest struct { ... }
type UserResponse struct { ... }

// Avoid - vague names
type Request struct { ... }
type Response struct { ... }

2. Add JSON Tags

Always use json tags for consistent field naming:

type User struct {
    UserID    string `json:"user_id"`     // snake_case in JSON
    FirstName string `json:"first_name"`  // consistent naming
    LastName  string `json:"last_name"`
}

3. Document with Comments

While Humus doesn’t currently extract comments into the OpenAPI spec, they help developers:

// User represents a registered user in the system
type User struct {
    // Unique identifier for the user
    ID string `json:"id"`

    // User's email address (must be unique)
    Email string `json:"email"`
}

4. Use Separate Request/Response Types

Don’t reuse types for both requests and responses:

// Good - separate types
type CreateUserRequest struct {
    Email string `json:"email"`
    Name  string `json:"name"`
}

type UserResponse struct {
    ID       string    `json:"id"`
    Email    string    `json:"email"`
    Name     string    `json:"name"`
    Created  time.Time `json:"created"`
}

// Avoid - single type for both
type User struct {
    ID      string    `json:"id,omitempty"`  // Confusing: required in response, not in request
    Email   string    `json:"email"`
    Name    string    `json:"name"`
    Created time.Time `json:"created,omitempty"`
}

5. Version Your API

Include version in the API title or base path:

api := rest.NewApi(
    "Bookstore API",
    "2.0.0",  // Semantic versioning
    // ...
)

// Or use versioned base paths
rest.Handle(
    http.MethodGet,
    rest.BasePath("/api/v2/books"),
    handler,
)

6. Validate the Generated Spec

Use OpenAPI validators to ensure correctness:

# Using openapi-generator-cli
openapi-generator-cli validate -i http://localhost:8080/openapi.json

# Using Spectral (advanced linting)
spectral lint http://localhost:8080/openapi.json

7. Cache the Spec for Performance

For high-traffic APIs, consider caching the generated spec:

// Cache the spec at startup
var cachedSpec []byte

func Init(ctx context.Context, cfg Config) (*rest.Api, error) {
    api := rest.NewApi("My API", "1.0.0", ...)

    // Pre-generate and cache the spec
    resp := httptest.NewRecorder()
    req := httptest.NewRequest(http.MethodGet, "/openapi.json", nil)
    api.ServeHTTP(resp, req)
    cachedSpec = resp.Body.Bytes()

    return api, nil
}

Limitations

No Description Fields

Currently, Humus does not extract Go comments into OpenAPI description fields. Type and field descriptions must be added manually if needed.

Limited Validation Constraints

While parameter validators (regex, required) appear in the spec, complex field-level validations (min/max length, numeric ranges) are not automatically reflected. Implement these in your handlers.

No Response Status Code Customization

Response types currently default to 200 OK. Custom status codes require implementing the TypedResponse interface directly.

Next Steps