# OpenAPI Aggregation This document describes how the StellaOps Gateway aggregates OpenAPI documentation from connected microservices into a unified specification. ## Overview The Gateway automatically generates a single OpenAPI 3.1.0 document that aggregates all endpoints from connected microservices. This provides: - **Unified API documentation**: All services documented in one place - **Dynamic updates**: Document regenerates when services connect/disconnect - **Standard compliance**: OpenAPI 3.1.0 with native JSON Schema draft 2020-12 support - **Multiple formats**: Available as JSON or YAML - **Efficient caching**: ETag-based caching with configurable TTL ### How It Works ``` ┌──────────────┐ HELLO ┌──────────────┐ GET /openapi.json ┌──────────────┐ │ Billing │ ──────────► │ │ ◄───────────────────── │ Client │ │ Service │ + schemas │ Gateway │ │ │ └──────────────┘ │ │ OpenAPI 3.1.0 │ │ │ │ ─────────────────────► │ │ ┌──────────────┐ HELLO │ │ unified document └──────────────┘ │ Inventory │ ──────────► │ │ │ Service │ + schemas └──────────────┘ └──────────────┘ ``` 1. Microservices send schemas and endpoint metadata via HELLO payload 2. Gateway stores this information in routing state 3. OpenAPI generator aggregates all connected services 4. Document is cached and served via HTTP endpoints --- ## Configuration ### OpenApiAggregationOptions Configure OpenAPI aggregation in your Gateway configuration: ```yaml # router.yaml or appsettings.yaml OpenApi: Title: "My API Gateway" Description: "Unified API for all microservices" Version: "2.0.0" ServerUrl: "https://api.example.com" CacheTtlSeconds: 60 Enabled: true LicenseName: "AGPL-3.0-or-later" ContactName: "API Team" ContactEmail: "api@example.com" TokenUrl: "/auth/token" ``` ### Configuration Reference | Property | Type | Default | Description | |----------|------|---------|-------------| | `Title` | `string` | `"StellaOps Gateway API"` | API title in OpenAPI info section | | `Description` | `string` | `"Unified API aggregating all connected microservices."` | API description | | `Version` | `string` | `"1.0.0"` | API version number | | `ServerUrl` | `string` | `"/"` | Base server URL | | `CacheTtlSeconds` | `int` | `60` | Cache time-to-live in seconds | | `Enabled` | `bool` | `true` | Enable/disable OpenAPI aggregation | | `LicenseName` | `string` | `"AGPL-3.0-or-later"` | License name in OpenAPI info | | `ContactName` | `string?` | `null` | Contact name (optional) | | `ContactEmail` | `string?` | `null` | Contact email (optional) | | `TokenUrl` | `string` | `"/auth/token"` | OAuth2 token endpoint URL | ### Disabling OpenAPI To disable OpenAPI aggregation entirely: ```yaml OpenApi: Enabled: false ``` --- ## Endpoints ### Discovery Endpoint ```http GET /.well-known/openapi ``` Returns metadata about the OpenAPI document: **Response:** ```json { "openapi_json": "/openapi.json", "openapi_yaml": "/openapi.yaml", "etag": "\"5d41402abc4b2a76b9719d911017c592\"", "generated_at": "2025-01-15T10:30:00.0000000Z" } ``` ### OpenAPI JSON ```http GET /openapi.json ``` Returns the full OpenAPI 3.1.0 specification in JSON format. **Headers:** - `Cache-Control: public, max-age=60` - `ETag: ""` - `Content-Type: application/json; charset=utf-8` **Conditional Request:** ```http GET /openapi.json If-None-Match: "5d41402abc4b2a76b9719d911017c592" ``` Returns `304 Not Modified` if content unchanged. ### OpenAPI YAML ```http GET /openapi.yaml ``` Returns the full OpenAPI 3.1.0 specification in YAML format. **Headers:** - `Cache-Control: public, max-age=60` - `ETag: ""` - `Content-Type: application/yaml; charset=utf-8` --- ## Security Mapping The Gateway automatically maps claim requirements to OpenAPI security schemes. ### Claim to Scope Mapping When endpoints define `RequiringClaims`, these are converted to OAuth2 scopes: ```csharp // Endpoint with claim requirements [StellaEndpoint("POST", "/invoices")] [RequireClaim("billing:write")] public sealed class CreateInvoiceEndpoint : IStellaEndpoint<...> ``` Becomes in OpenAPI: ```json { "paths": { "/invoices": { "post": { "security": [ { "BearerAuth": [], "OAuth2": ["billing:write"] } ] } } } } ``` ### Security Schemes The Gateway generates two security schemes: #### BearerAuth HTTP Bearer token authentication (always present): ```json { "BearerAuth": { "type": "http", "scheme": "bearer", "bearerFormat": "JWT", "description": "JWT Bearer token authentication" } } ``` #### OAuth2 Client credentials flow with collected scopes (only if endpoints have claims): ```json { "OAuth2": { "type": "oauth2", "flows": { "clientCredentials": { "tokenUrl": "/auth/token", "scopes": { "billing:write": "Access scope: billing:write", "billing:read": "Access scope: billing:read", "inventory:read": "Access scope: inventory:read" } } } } } ``` ### Scope Collection Scopes are automatically collected from all connected services. If multiple endpoints require the same claim, it appears only once in the scopes list. --- ## Generated Document Structure The aggregated OpenAPI document follows this structure: ```json { "openapi": "3.1.0", "info": { "title": "StellaOps Gateway API", "version": "1.0.0", "description": "Unified API aggregating all connected microservices.", "license": { "name": "AGPL-3.0-or-later" }, "contact": { "name": "API Team", "email": "api@example.com" } }, "servers": [ { "url": "/" } ], "paths": { "/invoices": { "post": { "operationId": "billing_invoices_POST", "tags": ["billing"], "summary": "Create invoice", "description": "Creates a new draft invoice", "security": [ { "BearerAuth": [], "OAuth2": ["billing:write"] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/billing_CreateInvoiceRequest" } } } }, "responses": { "200": { "description": "Success", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/billing_CreateInvoiceResponse" } } } }, "400": { "description": "Bad Request" }, "401": { "description": "Unauthorized" }, "404": { "description": "Not Found" }, "422": { "description": "Validation Error" }, "500": { "description": "Internal Server Error" } } } }, "/items": { "get": { "operationId": "inventory_items_GET", "tags": ["inventory"], "summary": "List items", "responses": { "200": { "description": "Success" } } } } }, "components": { "schemas": { "billing_CreateInvoiceRequest": { "type": "object", "required": ["customerId", "amount"], "properties": { "customerId": { "type": "string" }, "amount": { "type": "number" }, "description": { "type": ["string", "null"] }, "lineItems": { "type": "array", "items": { "$ref": "#/components/schemas/billing_LineItem" } } } }, "billing_CreateInvoiceResponse": { "type": "object", "required": ["invoiceId", "createdAt", "status"], "properties": { "invoiceId": { "type": "string" }, "createdAt": { "type": "string", "format": "date-time" }, "status": { "type": "string" } } }, "billing_LineItem": { "type": "object", "required": ["description", "amount"], "properties": { "description": { "type": "string" }, "amount": { "type": "number" }, "quantity": { "type": "integer", "default": 1 } } } }, "securitySchemes": { "BearerAuth": { "type": "http", "scheme": "bearer", "bearerFormat": "JWT", "description": "JWT Bearer token authentication" }, "OAuth2": { "type": "oauth2", "flows": { "clientCredentials": { "tokenUrl": "/auth/token", "scopes": { "billing:write": "Access scope: billing:write" } } } } } }, "tags": [ { "name": "billing", "description": "billing microservice (v1.0.0)" }, { "name": "inventory", "description": "inventory microservice (v2.0.0)" } ] } ``` ### Schema Prefixing Schemas are prefixed with the service name to prevent naming conflicts: | Service | Original Type | Prefixed Schema ID | |---------|--------------|-------------------| | `billing` | `CreateInvoiceRequest` | `billing_CreateInvoiceRequest` | | `inventory` | `GetItemResponse` | `inventory_GetItemResponse` | ### Tag Generation Tags are automatically generated from connected services: - Tag name: Service name (lowercase) - Tag description: Service description from `OpenApiInfo` or auto-generated ### Operation IDs Operation IDs follow the pattern: `{serviceName}_{path}_{method}` Example: `billing_invoices_POST` --- ## Cache Behavior ### TTL-Based Expiration The document cache expires based on `CacheTtlSeconds` (default: 60 seconds): ```yaml OpenApi: CacheTtlSeconds: 30 # More frequent regeneration ``` Setting `CacheTtlSeconds: 0` regenerates the document on every request (not recommended for production). ### Connection-Based Invalidation The cache is automatically invalidated when: 1. A new microservice connects (HELLO received) 2. A microservice disconnects (connection closed) This ensures the OpenAPI document always reflects currently connected services. ### ETag Consistency The ETag is computed from the document content hash (SHA256). This ensures: - Same content = same ETag - Content changes = new ETag - Clients can use conditional requests to avoid re-downloading unchanged documents ### Recommended Client Strategy ```javascript // Store ETag from previous response let cachedETag = localStorage.getItem('openapi-etag'); const response = await fetch('/openapi.json', { headers: cachedETag ? { 'If-None-Match': cachedETag } : {} }); if (response.status === 304) { // Use cached document return getCachedDocument(); } // Store new ETag and document localStorage.setItem('openapi-etag', response.headers.get('ETag')); const document = await response.json(); cacheDocument(document); return document; ``` --- ## Service Registration ### Microservice Options Configure service metadata that appears in OpenAPI: ```csharp services.AddStellaMicroservice(options => { options.ServiceName = "billing"; options.ServiceDescription = "Invoice and payment processing service"; options.ContactInfo = "billing-team@example.com"; }); ``` ### Service OpenAPI Info The `ServiceOpenApiInfo` is sent in the HELLO payload: ```json { "instance": { ... }, "endpoints": [ ... ], "schemas": { ... }, "openApiInfo": { "title": "billing", "description": "Invoice and payment processing service" } } ``` This description appears in the tag entry for the service. --- ## Troubleshooting ### Document Not Updating 1. Check `CacheTtlSeconds` - may need to wait for TTL expiration 2. Verify service connected successfully (check Gateway logs) 3. Force refresh by restarting the Gateway ### Missing Schemas 1. Ensure `[ValidateSchema]` attribute is applied to endpoints 2. Check for schema parsing errors in Gateway logs 3. Verify endpoint implements `IStellaEndpoint` ### Security Schemes Not Appearing 1. OAuth2 scheme only appears if endpoints have claim requirements 2. Check `RequiringClaims` is populated on endpoint descriptors 3. Verify claim types are being transmitted correctly --- ## See Also - [Schema Validation](schema-validation.md) - JSON Schema validation reference - [API Overview](../../api/overview.md) - General API conventions - [Gateway OpenAPI](../gateway/openapi.md) - Gateway OpenAPI implementation details