Skip to content

How to Generate System Architecture Diagrams from OpenAPI Specs

Problem

When I tried to auto-generate system architecture diagrams from my microservices codebase, I got a mess:

┌─────────────────────────────────────────────────────────────┐
│ OrderService │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Controller │ │ Validator │ │ Repository │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │ │
│ └──────────────────┴──────────────────┘ │
│ ▼ │
│ ┌────────────────────────┐ │
│ │ OrderValidatorUtils │ │
│ │ CurrencyFormatter │ │
│ │ TaxCalculator │ │
│ └────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ InventoryService │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Controller │ │ Validator │ │ Repository │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘

The diagram showed every internal class, utility function, and helper method. I couldn’t tell which services actually talked to each other versus which were just internal implementation details.

I tried parsing Java source files, then Python decorators, then TypeScript interfaces. Each language needed a different parser. Every time I refactored code, the diagrams broke. Import statements showed false dependencies that weren’t real service boundaries.

The core issue: source code exposes implementation, not architecture.

The solution

I switched to using OpenAPI specifications as the source of truth instead of source code. By feeding OpenAPI specs to Claude through an MCP code navigation server, I got clean diagrams that showed only service boundaries and real dependencies.

Here’s what I got:

C4Context
title "Microservices Architecture Context"
Person(user, "Customer", "Places orders")
System_Boundary(api, "API Gateway") {
System(order_service, "Order Service", "Manages orders")
System(inventory_service, "Inventory Service", "Stock management")
System(payment_service, "Payment Service", "Processes payments")
}
System_Ext(database, "Databases", "PostgreSQL cluster")
Rel(user, order_service, "HTTPS", "POST /orders")
Rel(order_service, inventory_service, "HTTPS", "Check stock")
Rel(order_service, payment_service, "HTTPS", "Charge payment")
Rel(order_service, database, "TCP", "Persist orders")
Rel(inventory_service, database, "TCP", "Update inventory")

You can see only service boundaries, no internal classes. Dependencies extracted from actual API contracts.

Why OpenAPI works better

OpenAPI specs capture API contracts, not implementation. This matters for four reasons:

1. API contracts expose actual boundaries

Only public endpoints appear in the spec. Internal classes like OrderController, OrderValidator, or CurrencyFormatter don’t exist at the API layer. The diagram shows what external services see, not how code is organized.

2. Language-agnostic

REST/GraphQL specs work the same whether services use Java, Python, Go, or TypeScript. I don’t need different parsers for each language.

3. Dependency clarity

When Service A references Service B’s schema via $ref, that’s a real dependency. When Java code imports an internal utility class, that’s not a service boundary.

4. Already maintained

Most microservices already expose OpenAPI docs at /openapi.json or /swagger.json. I don’t need to maintain separate tooling.

How I implemented it

Step 1: Gather OpenAPI specs

I accessed the OpenAPI spec from each service:

Terminal window
curl https://orders.api.example.com/openapi.json -o specs/order-service.json
curl https://inventory.api.example.com/openapi.json -o specs/inventory-service.json
curl https://payment.api.example.com/openapi.json -o specs/payment-service.json

Or you can export from API Gateway (Kong, AWS API Gateway, etc.) if all services route through one.

Step 2: Set up MCP server

I created an MCP server configuration to expose all specs:

mcp-server-config.json
{
"mcpServers": {
"openapi-nav": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-openapi"],
"env": {
"OPENAPI_DIRS": "/specs/order-service,/specs/inventory-service,/specs/payment-service"
}
}
}
}

This makes each service’s OpenAPI spec available to Claude as context through the MCP protocol.

Step 3: Generate diagrams with Claude

I prompted Claude:

Using the MCP code nav server, consume all OpenAPI specs and generate a C4 context diagram showing service dependencies.

Claude traced servers, paths, and $ref dependencies across all specs and output the Mermaid diagram. The key extraction logic:

  • servers field → service base URLs
  • paths field → exposed endpoints
  • $ref to external schemas → dependencies between services

For example, when order-service.json contains:

specs/order-service.json
{
"openapi": "3.0.0",
"info": { "title": "Order Service", "version": "1.0.0" },
"servers": [
{ "url": "https://orders.api.example.com" }
],
"paths": {
"/orders": {
"post": {
"summary": "Create order",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateOrderRequest"
}
}
}
},
"responses": {
"201": {
"description": "Order created",
"content": {
"application/json": {
"schema": {
"$ref": "https://inventory.api.example.com/openapi.json#/components/schemas/InventoryItem"
}
}
}
}
}
}
}
}
}

Claude extracts:

  • Service: order-service at orders.api.example.com
  • Dependency: inventory-service (via $ref to external schema)
  • Boundary: POST /orders endpoint

No internal classes leak through.

Step 4: Iterate and refine

After the initial diagram, I regenerated on API changes and added manual annotations for async/event-based dependencies that OpenAPI doesn’t capture (like Kafka topics or RabbitMQ queues).

Common mistakes I made

Including internal endpoints

I initially included /health, /metrics, and admin APIs in the diagrams, which cluttered them. I filtered these out by either excluding specific paths in the OpenAPI spec or post-processing the generated diagram.

Ignoring async messaging

OpenAPI doesn’t show Kafka or RabbitMQ dependencies. I manually annotated the Mermaid diagram to add message queues after the initial generation.

Over-diagramming

I tried to include every endpoint at first, which made the diagram unreadable. I learned to group by domain or bounded context instead, showing only key endpoints that represent service boundaries.

Version confusion

When services had v1 and v2 APIs, the diagram initially mixed them. I tagged diagrams with specific API versions and generated separate diagrams for each major version.

Alternative approach without MCP

If you don’t have an MCP server set up, you can export dependency data as JSON:

dependency-map.json
{
"services": [
{
"name": "order-service",
"basePath": "https://orders.api.example.com",
"endpoints": [
{
"method": "POST",
"path": "/orders",
"dependencies": ["inventory-service", "payment-service"]
}
]
},
{
"name": "inventory-service",
"basePath": "https://inventory.api.example.com",
"endpoints": [
{
"method": "GET",
"path": "/items/{id}",
"dependencies": []
}
]
}
]
}

Then prompt Claude:

Generate a C4 diagram from this dependency map showing service boundaries and external dependencies.

This works but requires manual maintenance of the dependency map, whereas the MCP approach consumes OpenAPI specs directly.

Why this matters

Onboarding accuracy

New developers see actual system boundaries, not internal classes. They understand which services talk to each other without getting lost in implementation details.

Documentation alignment

Diagrams match deployed APIs by definition. When the API changes, the OpenAPI spec updates, and regenerating the diagram keeps everything in sync.

Tool integration

This works with existing API documentation tooling like Swagger UI, Redoc, or Stoplight. No separate diagramming toolchain needed.

Change detection

OpenAPI changes automatically trigger diagram updates when you regenerate, keeping architecture docs current without manual updates.

Summary

In this post, I showed how to generate clean system architecture diagrams from OpenAPI specs using Claude and MCP servers. The key point is using API contracts as the source of truth instead of source code, which produces diagrams that show service boundaries rather than implementation details. This approach works especially well for microservices that already expose Swagger/OpenAPI documentation.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments