Class diagrams aren’t just for OO design docs.
They’re incredibly useful for API and service contracts:
- What fields exist in requests/responses?
- Which types are shared across services?
- What’s required vs optional?
- How do errors, pagination, and versions work?
Mermaid class diagrams let you keep that contract next to the code, review it in PRs, and update it as your API evolves.
This guide focuses on contract-first diagramming: DTOs, envelopes, error shapes, and service interfaces.
A quick refresher
Mermaid class diagrams start with classDiagram.
Source
|
1 2 3 4 5 6 7 |
classDiagram class Order { +string id +string status +decimal total } |
Rendered
Visibility markers (useful even for contracts)
+public (exposed in the contract)-private (internal detail)#protected (usually not needed for contracts)~package/internal
For API contracts, it’s common to use + for all public fields and omit internal ones.
1) Model request/response DTOs (the fastest win)
A clean “GET” response
Source
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
classDiagram class GetOrderResponse { +Order order +RequestMeta meta } class Order { +string id +string status +Money total +datetime createdAt } class Money { +string currency +decimal amount } class RequestMeta { +string requestId +datetime generatedAt } GetOrderResponse *-- Order Order *-- Money GetOrderResponse *-- RequestMeta |
Rendered
Quick relationship legend (use consistently)
A *-- Bcomposition (“A contains B”)A o-- Baggregation (“A has a B, but B may exist independently”)A --> Bassociation (“A references/uses B”)A <|-- Binheritance (“B is a type of A”)
For API DTOs, composition (*--) is usually the clearest default.
2) Multiplicity: show “one vs many” explicitly
This is how you prevent contract misunderstandings.
Source
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
classDiagram class Order { +string id +LineItem[] items } class LineItem { +string sku +int quantity +Money price } class Money { +string currency +decimal amount } Order "1" *-- "1..*" LineItem LineItem *-- Money |
Rendered
Tip: If it can be empty, say so (
0..*). If it’s optional, show it (0..1).
3) Design an “envelope” that scales (meta + paging)
Many APIs return a consistent wrapper. Document it once, reuse it everywhere.
Source
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
classDiagram class ApiResponse~T~ { +T data +RequestMeta meta +Error[] errors } class RequestMeta { +string requestId +datetime generatedAt +string version } class Error { +string code +string message +string traceId } ApiResponse~T~ *-- RequestMeta ApiResponse~T~ o-- Error |
Rendered
Pagination as a reusable contract
Source
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
classDiagram class PageMeta { +int limit +string nextCursor +bool hasMore } class ListOrdersResponse { +Order[] orders +PageMeta page } class Order { +string id +string status +Money total } class Money { +string currency +decimal amount } ListOrdersResponse *-- PageMeta ListOrdersResponse "1" *-- "0..*" Order Order *-- Money |
Rendered
4) Errors that don’t surprise clients
In incidents, unclear error shapes cause secondary failures (bad retries, missing IDs, no correlation).
Document your error model explicitly.
Source
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
classDiagram class Problem { +string type +string title +int status +string detail +string instance +string traceId } class ValidationProblem { +FieldError[] fieldErrors } class FieldError { +string field +string message } Problem <|-- ValidationProblem ValidationProblem *-- FieldError |
Rendered
If you include traceId / requestId in errors, clients can paste it into a ticket and you can find the exact failure fast.
5) Service interfaces: show who calls what (without drawing architecture)
Class diagrams can document service interfaces and dependencies without turning into a full architecture diagram.
A small “contract surface” for a service
Source
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
classDiagram class OrdersService { +GetOrder(id: string) Order +ListOrders(customerId: string, limit: int, cursor: string) Order[] +CreateOrder(cmd: CreateOrderCommand) Order } class CreateOrderCommand { +string customerId +LineItem[] items +string idempotencyKey } class LineItem { +string sku +int quantity } OrdersService --> CreateOrderCommand CreateOrderCommand *-- LineItem |
Rendered
Note the
idempotencyKey—it’s part of the contract. If it exists, document it.
6) Versioning strategies, shown clearly
Versioning becomes messy when “v1 vs v2” isn’t explicit. Class diagrams help you show compatibility.
Source
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
classDiagram class OrderV1 { +string id +string status +decimal total } class OrderV2 { +string id +string status +Money total +datetime createdAt } class Money { +string currency +decimal amount } OrderV1 <|-- OrderV2 OrderV2 *-- Money |
Rendered
If a “v2” is not truly a subtype of “v1”, don’t force inheritance—use separate types and show mapping in a note or a short sequence diagram.
7) Styling: use classes to highlight contract vs internal details
When you’re documenting contracts, you often want to distinguish public API models from internal-only models.
Source
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
classDiagram classDef public fill:#eef,stroke:#446,stroke-width:1px; classDef internal fill:#f5f5f5,stroke:#888,stroke-dasharray: 5 5; class PublicOrder { +string id +string status +Money total } class InternalOrderRow { -string pk -string sk -string payloadJson } class Money { +string currency +decimal amount } PublicOrder *-- Money PublicOrder --> InternalOrderRow : derived from class PublicOrder public class Money public class InternalOrderRow internal |
Rendered
8) A blog-ready checklist for contract diagrams
Before you publish:
- Keep one diagram focused on a single endpoint or service surface.
- Name types the way they appear in the API:
CreateOrderCommand,Problem,PageMeta. - Show multiplicity (
0..1,1..*) anywhere it matters. - Prefer composition (
*--) for “contains” relationships in payloads. - Include operationally useful fields like requestId / traceId / idempotencyKey.
- If the diagram gets too big, split it into:
- Public contract (DTOs + envelopes)
- Internal model (storage rows, aggregates)
Where to go next
If class diagrams are working for you, the next Mermaid diagram type to learn for contracts is ER diagrams—they’re excellent for showing persistence models and relationships without mixing them into your API DTOs.




