13 KiB
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 └──────────────┘
└──────────────┘
- Microservices send schemas and endpoint metadata via HELLO payload
- Gateway stores this information in routing state
- OpenAPI generator aggregates all connected services
- Document is cached and served via HTTP endpoints
Configuration
OpenApiAggregationOptions
Configure OpenAPI aggregation in your Gateway configuration:
# 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: "BUSL-1.1"
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 |
"BUSL-1.1" |
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:
OpenApi:
Enabled: false
Endpoints
Discovery Endpoint
GET /.well-known/openapi
Returns metadata about the OpenAPI document:
Response:
{
"openapi_json": "/openapi.json",
"openapi_yaml": "/openapi.yaml",
"etag": "\"5d41402abc4b2a76b9719d911017c592\"",
"generated_at": "2025-01-15T10:30:00.0000000Z"
}
OpenAPI JSON
GET /openapi.json
Returns the full OpenAPI 3.1.0 specification in JSON format.
Headers:
Cache-Control: public, max-age=60ETag: "<content-hash>"Content-Type: application/json; charset=utf-8
Conditional Request:
GET /openapi.json
If-None-Match: "5d41402abc4b2a76b9719d911017c592"
Returns 304 Not Modified if content unchanged.
OpenAPI YAML
GET /openapi.yaml
Returns the full OpenAPI 3.1.0 specification in YAML format.
Headers:
Cache-Control: public, max-age=60ETag: "<content-hash>"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:
// Endpoint with claim requirements
[StellaEndpoint("POST", "/invoices")]
[RequireClaim("billing:write")]
public sealed class CreateInvoiceEndpoint : IStellaEndpoint<...>
Becomes in OpenAPI:
{
"paths": {
"/invoices": {
"post": {
"security": [
{
"BearerAuth": [],
"OAuth2": ["billing:write"]
}
]
}
}
}
}
Security Schemes
The Gateway generates two security schemes:
BearerAuth
HTTP Bearer token authentication (always present):
{
"BearerAuth": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT",
"description": "JWT Bearer token authentication"
}
}
OAuth2
Client credentials flow with collected scopes (only if endpoints have claims):
{
"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.
Legacy HELLO Compatibility
x-stellaops-gateway-auth.requiresAuthentication is emitted from the Gateway's effective authorization semantics, not only the raw endpoint flag.
- If a microservice HELLO payload comes from an older router-common contract that does not include
requiresAuthentication, the Gateway fails closed. - For
allowAnonymous: falseendpoints with no explicit auth flag, the Gateway treats the route as authenticated-only. - Public routes must be explicitly marked
AllowAnonymousin the microservice to avoid accidental protection.
Generated Document Structure
The aggregated OpenAPI document follows this structure:
{
"openapi": "3.1.0",
"info": {
"title": "StellaOps Gateway API",
"version": "1.0.0",
"description": "Unified API aggregating all connected microservices.",
"license": {
"name": "BUSL-1.1"
},
"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
OpenApiInfoor 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):
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:
- A new microservice connects (HELLO received)
- 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
// 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:
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:
{
"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
- Check
CacheTtlSeconds- may need to wait for TTL expiration - Verify service connected successfully (check Gateway logs)
- Force refresh by restarting the Gateway
Missing Schemas
- Ensure
[ValidateSchema]attribute is applied to endpoints - Check for schema parsing errors in Gateway logs
- Verify endpoint implements
IStellaEndpoint<TRequest, TResponse>
Security Schemes Not Appearing
- OAuth2 scheme only appears if endpoints have claim requirements
- Check
RequiringClaimsis populated on endpoint descriptors - Verify claim types are being transmitted correctly
See Also
- Schema Validation - JSON Schema validation reference
- API Overview - General API conventions
- Gateway OpenAPI - Gateway OpenAPI implementation details