Complete Components

Combine request body and response components in a complete API with validation and error handling.

Full source

File: [pwsh/tutorial/examples/10.6-OpenAPI-Components-RequestBody-Response.ps1][10.6-OpenAPI-Components-RequestBody-Response.ps1]

<#
    Sample: OpenAPI RequestBody & Response Components
    Purpose: Demonstrate complete components integration with request bodies and response schemas.
    File:    10.6-OpenAPI-Components-RequestBody-Response.ps1
    Notes:   Shows component wrapping, CRUD operations, and complete error handling.
#>
param(
    [int]$Port = 5000,
    [IPAddress]$IPAddress = [IPAddress]::Loopback
)


# --- Logging / Server ---

New-KrLogger | Add-KrSinkConsole |
    Set-KrLoggerLevel -Value Debug |
    Register-KrLogger -Name 'console' -SetAsDefault

$srv = New-KrServer -Name 'OpenAPI RequestBody & Response Components' -PassThru
Add-KrEndpoint -Port $Port -IPAddress $IPAddress
# =========================================================
#                 TOP-LEVEL OPENAPI
# =========================================================

Add-KrOpenApiInfo -Title 'Complete Components API' `
    -Version '1.0.0' `
    -Description 'Demonstrates complete reusable components: request bodies and responses together.'


Add-KrOpenApiTag -Name 'orders' -Description 'Order management operations'

# =========================================================
#                      COMPONENT SCHEMAS
# =========================================================

# Request schema for creating an order
[OpenApiSchemaComponent(Required = ('productId', 'quantity'))]
class CreateOrderRequest {
    [OpenApiPropertyAttribute(Description = 'Product ID', Example = 1)]
    [long]$productId

    [OpenApiPropertyAttribute(Description = 'Quantity ordered', Minimum = 1, Example = 5)]
    [int]$quantity

    [OpenApiPropertyAttribute(Description = 'Customer email', Format = 'email', Example = 'customer@example.com')]
    [string]$customerEmail

    [OpenApiPropertyAttribute(Description = 'Shipping address')]
    [string]$shippingAddress
}

# Response schema for order data
[OpenApiSchemaComponent(Required = ('orderId', 'productId', 'quantity', 'status', 'totalPrice'))]
class OrderResponse {
    [OpenApiPropertyAttribute(Description = 'Order ID', Example = 'a54a57ca-36f8-421b-a6b4-2e8f26858a4c')]
    [Guid]$orderId

    [OpenApiPropertyAttribute(Description = 'Product ID', Format = 'int64', Example = 1)]
    [long]$productId

    [OpenApiPropertyAttribute(Description = 'Quantity ordered', Example = 5)]
    [int]$quantity

    [OpenApiPropertyAttribute(Description = 'Order status'  )]
    [ValidateSet('pending', 'processing', 'shipped', 'delivered')]
    [string]$status

    [OpenApiPropertyAttribute(Description = 'Total price', Format = 'double', Example = 499.95)]
    [double]$totalPrice

    [OpenApiPropertyAttribute(Description = 'Order creation date', Format = 'date-time')]
    [string]$createdAt

    [OpenApiPropertyAttribute(Description = 'Expected delivery date', Format = 'date')]
    [string]$expectedDelivery
}

# Error response schema
[OpenApiSchemaComponent(Required = ('code', 'message'))]
class ErrorDetail {
    [OpenApiPropertyAttribute(Description = 'Error code', Example = 'INVALID_QUANTITY')]
    [string]$code

    [OpenApiPropertyAttribute(Description = 'Error message', Example = 'Quantity must be greater than zero')]
    [string]$message

    [OpenApiPropertyAttribute(Description = 'Field that caused the error')]
    [string]$field

    [OpenApiPropertyAttribute(Description = 'Additional context')]
    [string]$details
}

# =========================================================
#     COMPONENT REQUEST BODIES & RESPONSES (Reusable)
# =========================================================

# CreateOrderRequest: RequestBody component
[OpenApiRequestBodyComponent(
    Description = 'Order creation payload',
    IsRequired = $true,
    ContentType = 'application/json'
)]
class CreateOrderRequestBody:CreateOrderRequest {}

# OrderResponse: ResponseComponent
[OpenApiResponseComponent(JoinClassName = '-', Description = 'Order data')]
class OrderResponseComponent {
    [OpenApiResponse(Description = 'Order successfully retrieved or created', ContentType = 'application/json')]
    [OrderResponse]$Default
}

# ErrorResponse: ResponseComponent (reusable for multiple error codes)
[OpenApiResponseComponent(JoinClassName = '-', Description = 'Error response')]
class ErrorResponseComponent {
    [OpenApiResponse(Description = 'Request validation error', ContentType = 'application/json')]
    [ErrorDetail]$Default
}

# =========================================================
#                 ROUTES / OPERATIONS
# =========================================================

Enable-KrConfiguration

Add-KrApiDocumentationRoute -DocumentType Swagger
Add-KrApiDocumentationRoute -DocumentType Redoc


<#
.SYNOPSIS
    Create a new order.
.DESCRIPTION
    Creates a new order using the reusable CreateOrderRequestBody component.
    Returns OrderResponse on success or ErrorDetail on failure.
.PARAMETER body
    Order creation request using CreateOrderRequestBody component
.NOTES
    Tags: orders
    POST /orders: Create order
#>
function createOrder {
    [OpenApiPath(HttpVerb = 'post', Pattern = '/orders')]
    [OpenApiResponse(StatusCode = '201', Description = 'Order created successfully', Schema = [OrderResponse], ContentType = ('application/json', 'application/xml'))]
    [OpenApiResponse(StatusCode = '400', Description = 'Invalid input', Schema = [ErrorDetail], ContentType = ('application/json', 'application/xml'))]
    param(
        [OpenApiRequestBody(ContentType = ('application/json', 'application/xml', 'application/x-www-form-urlencoded'))]
        [CreateOrderRequestBody]$body
    )

    # Validate required fields
    if (-not $body.productId -or -not $body.quantity) {
        $myError = [ErrorDetail] @{
            code = 'MISSING_FIELDS'
            message = 'productId and quantity are required'
            field = 'productId,quantity'
        }
        Write-KrResponse $myError -StatusCode 400
        return
    }

    # Validate quantity
    if ([int]$body.quantity -le 0) {
        $myError = [ErrorDetail]@{
            code = 'INVALID_QUANTITY'
            message = 'Quantity must be greater than zero'
            field = 'quantity'
            details = "Provided: $($body.quantity)"
        }
        Write-KrResponse $myError -StatusCode 400
        return
    }

    # Create order response
    $unitPrice = 99.99
    $totalPrice = [int]$body.quantity * $unitPrice
    $response = [OrderResponse] @{
        orderId = [System.Guid]::NewGuid().ToString()
        productId = [long]$body.productId
        quantity = [int]$body.quantity
        status = 'pending'
        totalPrice = $totalPrice
        createdAt = (Get-Date).ToUniversalTime().ToString('o')
        expectedDelivery = (Get-Date).AddDays(5).ToString('yyyy-MM-dd')
    }

    Write-KrResponse $response -StatusCode 201
}


<#
.SYNOPSIS
    Get an order by ID.
.DESCRIPTION
    Retrieves order details using the OrderResponse component.
.PARAMETER orderId
    The order ID to retrieve
.NOTES
    GET /orders/{orderId}: Get order
#>
function getOrder {
    [OpenApiPath(HttpVerb = 'get', Pattern = '/orders/{orderId}')]
    [OpenApiResponse(StatusCode = '200', Description = 'Order found', Schema = [OrderResponse], ContentType = ('application/json', 'application/xml'))]
    [OpenApiResponse(StatusCode = '400', Description = 'Invalid order ID', Schema = [ErrorDetail], ContentType = ('application/json', 'application/xml'))]
    param(
        [OpenApiParameter(In = [OaParameterLocation]::Path, Required = $true)]
        [Guid]$orderId
    )

    # Validate UUID format
    if (-not [System.Guid]::TryParse($orderId, [ref][System.Guid]::Empty)) {
        $myError = [ErrorDetail] @{
            code = 'INVALID_ORDER_ID'
            message = 'Invalid order ID format'
            field = 'orderId'
            details = 'Order ID must be a valid UUID'
        }
        Write-KrResponse $myError -StatusCode 400
        return
    }

    # Mock order data
    $response = [OrderResponse]@{
        orderId = $orderId
        productId = 1
        quantity = 5
        status = 'processing'
        totalPrice = 499.95
        createdAt = (Get-Date).AddDays(-1).ToUniversalTime().ToString('o')
        expectedDelivery = (Get-Date).AddDays(4).ToString('yyyy-MM-dd')
    }

    Write-KrResponse $response -StatusCode 200
}


<#
.SYNOPSIS
    Update an existing order.
.DESCRIPTION
    Updates order details using the CreateOrderRequestBody component.
.PARAMETER orderId
    The order ID to update
.PARAMETER body
    Updated order data
.NOTES
    PUT /orders/{orderId}: Update order
#>
function updateOrder {
    [OpenApiPath(HttpVerb = 'put', Pattern = '/orders/{orderId}')]
    [OpenApiResponse(StatusCode = '200', Description = 'Order updated successfully', Schema = [OrderResponse], ContentType = ('application/json', 'application/xml'))]
    [OpenApiResponse(StatusCode = '400', Description = 'Invalid input', Schema = [ErrorDetail], ContentType = ('application/json', 'application/xml'))]
    param(
        [OpenApiParameter(In = [OaParameterLocation]::Path, Required = $true, Example = 'a54a57ca-36f8-421b-a6b4-2e8f26858a4c')]
        [guid]$orderId,
        [OpenApiRequestBody(ContentType = ('application/json', 'application/xml', 'application/x-www-form-urlencoded'))]
        [CreateOrderRequestBody]$body
    )

    # Validate quantity if provided
    if ($body.quantity -and [int]$body.quantity -le 0) {
        $myError = [ErrorDetail] @{
            code = 'INVALID_QUANTITY'
            message = 'Quantity must be greater than zero'
            field = 'quantity'
        }
        Write-KrResponse $myError -StatusCode 400
        return
    }

    # Updated response
    $unitPrice = 99.99
    $quantity = $body.quantity -as [int] ? [int]$body.quantity : 5
    $response = [OrderResponse]@{
        orderId = $orderId
        productId = $body.productId -as [long] ? [long]$body.productId : 1
        quantity = $quantity
        status = 'processing'
        totalPrice = $quantity * $unitPrice
        createdAt = (Get-Date).AddDays(-1).ToUniversalTime().ToString('o')
        expectedDelivery = (Get-Date).AddDays(4).ToString('yyyy-MM-dd')
    }

    Write-KrResponse $response -StatusCode 200
}

# =========================================================
#                OPENAPI DOC ROUTE / BUILD
# =========================================================

Add-KrOpenApiRoute

Build-KrOpenApiDocument
Test-KrOpenApiDocument

# =========================================================
#                      RUN SERVER
# =========================================================

Start-KrServer -Server $srv -CloseLogsOnExit


Step-by-step

  1. Logging: Register console logger as default.
  2. Server: Create server named ‘OpenAPI RequestBody & Response Components’.
  3. OpenAPI info: Add title, description, and tags.
  4. Define CreateOrderRequest schema with productId and quantity.
  5. Define OrderResponse schema with order details and status.
  6. Define ErrorDetail schema with code, message, field, and details.
  7. Create CreateOrderRequestBody component from CreateOrderRequest.
  8. Create OrderResponseComponent wrapping OrderResponse.
  9. Create ErrorResponseComponent wrapping ErrorDetail.
  10. POST endpoint: Accept CreateOrderRequestBody, validate, return OrderResponse or ErrorDetail.
  11. GET endpoint: Retrieve order, validate UUID format, return OrderResponse or ErrorDetail.
  12. PUT endpoint: Update order using CreateOrderRequestBody, return OrderResponse or ErrorDetail.
  13. Build and test OpenAPI document.

Try it

# Create order (POST with CreateOrderRequestBody)
curl -X POST http://127.0.0.1:5000/orders `
  -H "Content-Type: application/json" `
  -d '{
    "productId": 1,
    "quantity": 5,
    "customerEmail": "customer@example.com",
    "shippingAddress": "123 Main St, Anytown"
  }'

# Get order (returns OrderResponse)
curl -i http://127.0.0.1:5000/orders/a54a57ca-36f8-421b-a6b4-2e8f26858a4c

# Get order with invalid ID (returns ErrorDetail)
curl -i http://127.0.0.1:5000/orders/invalid-id

# Update order (PUT with CreateOrderRequestBody)
curl -X PUT http://127.0.0.1:5000/orders/a54a57ca-36f8-421b-a6b4-2e8f26858a4c `
  -H "Content-Type: application/json" `
  -d '{
    "productId": 2,
    "quantity": 10,
    "customerEmail": "customer@example.com"
  }'

# Create order with invalid quantity (returns ErrorDetail)
curl -X POST http://127.0.0.1:5000/orders `
  -H "Content-Type: application/json" `
  -d '{
    "productId": 1,
    "quantity": 0
  }'

PowerShell create order:

$orderPayload = @{
    productId        = 1
    quantity         = 5
    customerEmail    = 'customer@example.com'
    shippingAddress  = '123 Main St, Anytown'
} | ConvertTo-Json

$response = Invoke-WebRequest -Uri http://127.0.0.1:5000/orders `
    -Method Post `
    -Headers @{ 'Content-Type' = 'application/json' } `
    -Body $orderPayload

$response.Content | ConvertFrom-Json

Complete API Pattern

POST /orders
├─ Request: CreateOrderRequestBody (required)
├─ Response 201: OrderResponseComponent
└─ Response 400: ErrorResponseComponent

GET /orders/{orderId}
├─ Parameter: orderId (path, UUID format)
├─ Response 200: OrderResponseComponent
└─ Response 400: ErrorResponseComponent

PUT /orders/{orderId}
├─ Parameter: orderId (path, UUID format)
├─ Request: CreateOrderRequestBody (required)
├─ Response 200: OrderResponseComponent
└─ Response 400: ErrorResponseComponent

Validation Patterns

# Validate required fields
if (-not $body.productId -or -not $body.quantity) {
    $error = @{
        code    = 'MISSING_FIELDS'
        message = 'productId and quantity are required'
        field   = 'productId,quantity'
    }
    Write-KrJsonResponse $error -StatusCode 400
    return
}

# Validate quantity constraint
if ([int]$body.quantity -le 0) {
    $error = @{
        code    = 'INVALID_QUANTITY'
        message = 'Quantity must be greater than zero'
        field   = 'quantity'
        details = "Provided: $($body.quantity)"
    }
    Write-KrJsonResponse $error -StatusCode 400
    return
}

# Validate UUID format
if (-not [System.Guid]::TryParse($orderId, [ref][System.Guid]::Empty)) {
    $error = @{
        code    = 'INVALID_ORDER_ID'
        message = 'Invalid order ID format'
        field   = 'orderId'
        details = 'Order ID must be a valid UUID'
    }
    Write-KrJsonResponse $error -StatusCode 400
    return
}

Key Concepts

  • Integration: RequestBody and Response components work together for complete endpoint documentation.
  • Error Handling: Use consistent ErrorDetail schema for all error responses.
  • Validation: Validate inputs and return appropriate error codes (MISSING_FIELDS, INVALID_QUANTITY, etc.).
  • Status Codes: Use 201 for creation, 200 for success, 400 for validation errors.
  • Traceability: Include operation IDs (UUIDs) in responses for tracking.

Troubleshooting

Issue: Components not reused across endpoints.

  • Solution: Ensure [OpenApiRequestBodyRef] and response attributes use the same component class references across all functions.

Issue: Validation errors not returning ErrorDetail schema.

  • Solution: Verify error response uses Schema = [ErrorDetail] and Write-KrJsonResponse returns matching structure.

Issue: UUID validation failing.

  • Solution: Use [System.Guid]::TryParse($id, [ref][System.Guid]::Empty) to validate UUID format before processing.

References


Previous / Next

Previous: Response Components Next: Tags and External Docs