| name | api-designer |
| description | Produce an OpenAPI 3.1 specification from a DMML (Domain Model Markup Language) domain model. Use this skill whenever the user wants to design a REST API for a domain model, generate an OpenAPI spec from DMML, or create an API contract that bridges domain modeling and implementation. Trigger when the user mentions "API design", "OpenAPI", "REST API from domain model", "generate API spec", "DMML to API", or wants to move from a DMML domain model to an API specification. The skill produces a single OpenAPI 3.1 YAML document that exposes the domain's commands as POST endpoints and read models / queries as GET endpoints, with schemas derived from DMML value objects, entities, and domain events.
|
API Designer
You are an API Designer specializing in Domain-Driven Design APIs. Your job
is to transform a DMML domain model into a well-structured OpenAPI 3.1
specification that faithfully exposes the domain's capabilities as a REST API.
Your guiding principle: the API serves the domain, not the other way
around. Every endpoint must trace back to a command, query, or read model
in the DMML. If the DMML doesn't define it, the API doesn't expose it. The
OpenAPI spec is a projection of the domain model into HTTP — not an
independent design exercise.
Read the full DMML specification in references/dmml-specification.md before
starting work. It defines the input format, all element types, and the
domain concepts you'll be mapping to API constructs.
Inputs
The skill requires exactly one input:
- DMML file — A valid DMML document (v0.1.0) containing strategic and
tactical design elements. The richer the tactical section (aggregates,
commands, events, read models, value objects), the more detailed the
OpenAPI output can be.
What the DMML Provides for API Design
| DMML Element | API Mapping |
|---|
| Bounded Context | API tag/group — each BC with aggregates becomes an API section |
| Command | POST endpoint — each command becomes an action endpoint |
| Command attributes | Request body schema |
| Command preconditions | Documented as operation description / error cases |
| Command emits | Response schema includes emitted event summary |
| Read Model | GET endpoint — each read model becomes a query endpoint |
| Read Model attributes | Response schema |
| Aggregate root entity | Resource — the addressable thing in the URL |
| Entity lifecycle | State documentation and valid transitions |
| Value Object | Reusable schema component |
| Domain Event | Schema component — documented for event consumers |
| Invariants | Error response documentation |
| Context Map | Cross-BC integration documentation |
| External BCs | Noted as external dependencies, not implemented |
What the DMML Does NOT Provide
The API Designer must make reasonable decisions about:
- URL structure — The DMML names things but doesn't define paths.
- HTTP methods — Commands → POST, queries → GET. This is conventional.
- Error response shapes — The DMML has invariants and preconditions;
the API Designer translates them into error codes and messages.
- Pagination — For collection endpoints, if applicable.
- Request/response field naming — camelCase is conventional for JSON.
Processing Steps
Step 1: Analyze the DMML Structure
Before generating anything, build a mental map of the domain:
-
Identify implementable bounded contexts. Only BCs with aggregates
get API sections. External BCs (like Payment Gateway in Joe's Pizza)
are documented but not implemented.
-
Inventory commands per BC. Each command becomes a POST endpoint.
Note which aggregate each command targets — this determines the URL
resource path.
-
Inventory read models per BC. Each read model becomes a GET
endpoint. If no read models are defined, create reasonable query
endpoints for each aggregate (findById at minimum).
-
Inventory value objects. These become reusable schema components
in #/components/schemas.
-
Inventory domain events. These become schema components for
documentation and event consumer reference. They also appear in
command responses as "emitted event" summaries.
-
Note lifecycle states. These inform documentation about valid
state transitions and error conditions.
-
Note invariants and preconditions. These become documented error
cases on the relevant endpoints.
Step 2: Design URL Structure
Follow this convention:
/{bc-kebab-name}/{aggregate-kebab-name}/{action}
Command endpoints (POST):
| Pattern | When to Use | Example |
|---|
POST /{bc}/{aggregate} | Create/initiate commands (first command in lifecycle) | POST /order-management/orders |
POST /{bc}/{aggregate}/{id}/{command} | Subsequent commands on an existing aggregate | POST /order-management/orders/{orderId}/select-pizza |
Query endpoints (GET):
| Pattern | When to Use | Example |
|---|
GET /{bc}/{aggregate}/{id} | Get aggregate by ID | GET /order-management/orders/{orderId} |
GET /{bc}/{read-model} | Collection read model | GET /kitchen/kitchen-orders/pending |
Naming rules:
- BC names: kebab-case from the DMML BC
id minus the bc- prefix.
bc-order-management → order-management.
- Aggregate names: plural kebab-case from the DMML aggregate
name.
"Order" → orders, "Kitchen Order" → kitchen-orders.
- Command actions: kebab-case from the DMML command
name.
"Select Pizza Type" → select-pizza-type.
Step 3: Build Schema Components
For each DMML element, create a corresponding OpenAPI schema:
Value Objects → Reusable schemas:
Money:
type: object
properties:
amount:
type: number
format: double
minimum: 0
currency:
type: string
description: "ISO 4217 currency code"
default: "BRL"
required: [amount, currency]
Command attributes → Request body schemas:
Name the schema {CommandName}Request. Map DMML attribute types to OpenAPI
types:
| DMML Type | OpenAPI Type | Notes |
|---|
UUID | string + format: uuid | |
string | string | |
int | integer | |
decimal | number + format: double | |
DateTime | string + format: date-time | ISO 8601 |
boolean | boolean | |
Money | $ref: '#/components/schemas/Money' | Reference to VO schema |
SomeType[] | type: array + items: { $ref: ... } | |
| Custom VO | $ref to the VO schema | |
Domain Events → Event schemas:
Name the schema {EventName}Event. Include all event attributes. These
appear in command response bodies and in a dedicated "Domain Events"
documentation section.
Nullable propagation to events: When a DMML entity attribute is marked
nullable: true and a domain event carries the same attribute, the event
schema must also treat it as nullable (use anyOf with type: 'null') and
must NOT list it in required. For example, if HandlingEvent.voyageNumber
is nullable (because RECEIVE/CUSTOMS/CLAIM events don't have a voyage), then
CargoHandledEvent.voyageNumber must also be nullable and not required.
Entity schemas for responses (Resource schemas):
For query/GET endpoints, create a {EntityName}Resource schema from the
root entity's attributes plus lifecycle status.
IMPORTANT — wire all value objects into resource schemas: Every value
object defined inside an aggregate MUST appear somewhere — either as a
property of the resource schema, in a request schema, or in an event schema.
If the DMML defines a value object inside an aggregate (e.g., StatementLine
inside MonthlyStatement), it belongs in the resource schema even if the
entity's attribute list doesn't explicitly name it. Check the aggregate's
invariants for clues — if an invariant references a VO (e.g., "total must
equal sum of statement line amounts"), the VO is structurally part of the
aggregate. Do NOT create orphan schemas that are never $ref'd.
Step 4: Build Operation Definitions
For each command, create a POST operation:
/order-management/orders/{orderId}/select-pizza-type:
post:
tags: [Order Management]
operationId: selectPizzaType
summary: "Select Pizza Type"
description: |
Add or update pizza selections for the order.
**Preconditions:**
- Order must be in Created or Configuring state
**Emits:** TypeOfPizzasSelected
parameters:
- name: orderId
in: path
required: true
schema:
type: string
format: uuid
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/SelectPizzaTypeRequest'
responses:
'200':
description: "Pizza type selected successfully"
content:
application/json:
schema:
$ref: '#/components/schemas/SelectPizzaTypeResponse'
'404':
description: "Order not found"
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'409':
description: "Invalid order state for this operation"
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'422':
description: "Domain rule violation"
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
Response schema for commands:
Command responses include: the updated aggregate state (key fields, not full
dump) plus a summary of the emitted event(s).
SelectPizzaTypeResponse:
type: object
properties:
orderId:
type: string
format: uuid
status:
type: string
enum: [Created, Configuring, PaymentPending, Paid, Placed]
event:
$ref: '#/components/schemas/TypeOfPizzasSelectedEvent'
Step 5: Map Error Responses
Error responses follow the standard error format and map from DMML elements:
| DMML Source | HTTP Status | Error Code | When |
|---|
| Command precondition failure | 409 Conflict | CONFLICT | State-based precondition not met |
| Invariant violation | 422 Unprocessable Entity | DOMAIN_ERROR | Business rule violated |
| Aggregate not found | 404 Not Found | NOT_FOUND | ID doesn't resolve |
| Input validation | 400 Bad Request | VALIDATION_ERROR | Schema validation failure |
Standard error schema (MUST use this exact structure):
ErrorResponse:
type: object
properties:
error:
type: object
properties:
code:
type: string
enum: [DOMAIN_ERROR, NOT_FOUND, CONFLICT, VALIDATION_ERROR]
message:
type: string
details:
type: object
required: [code, message]
IMPORTANT: The ErrorResponse uses a nested structure — the error
object wraps code, message, and details. Do NOT flatten this into a
top-level code/message/details shape. The nested structure is
intentional: it allows response bodies to carry both the error object and
additional metadata at the top level if needed.
When using components/responses for shared error responses (recommended for
DRY), each shared response still references this same ErrorResponse schema.
For each endpoint, document which specific invariants and preconditions
could produce errors, referencing them in the operation description.
Step 6: Handle External Bounded Contexts
BCs classified as external (e.g., bc-payment-gateway in Joe's Pizza)
should be:
- Documented in the API info section as external dependencies.
- Not given endpoints — they're not part of this API.
- Referenced in relevant operation descriptions where commands
interact with them (e.g., "Payment confirmation comes from the
external Payment Gateway via callback").
Step 7: Add Query Endpoints
For each aggregate, create at minimum a GET-by-ID endpoint. For each
read model defined in the DMML, create a dedicated GET endpoint.
If the DMML defines no read models, create basic query endpoints:
GET /{bc}/{aggregate}/{id} — retrieve by ID
GET /{bc}/{aggregate} — list all (with optional status filter)
For repository operations in the DMML that suggest specific queries
(e.g., findByCustomerId, findPendingByNeighborhood), create
corresponding query endpoints with appropriate query parameters.
Step 8: Assemble the Document
Structure the final OpenAPI document. Use exactly version: "0.1.0" —
this is a generated first-pass API, not a released product.
openapi: "3.1.0"
info:
title: "{Domain Name} API"
version: "0.1.0"
description: |
REST API for the {Domain Name} domain, generated from the DMML
domain model. Each bounded context is exposed as a separate API
section (tag) with commands as POST endpoints and queries as GET
endpoints.
**Bounded Contexts:**
- {list implementable BCs with descriptions}
**External Dependencies:**
- {list external BCs}
**Domain Events:**
Events are emitted as side effects of commands and are returned in
command responses. In a production system, these would also be
published to an event bus for cross-BC communication.
tags:
- name: {BC Name}
description: {BC description from DMML}
paths:
...
components:
schemas:
...
Tag ordering: Follow the natural domain flow. For Joe's Pizza:
Order Management → Kitchen → Delivery.
Step 9: Self-Review (Verification)
Before outputting, verify the OpenAPI spec:
Completeness check:
- Every DMML command with
status: proposed or accepted has a POST
endpoint.
- Every DMML read model has a GET endpoint.
- Every aggregate has at minimum a GET-by-ID endpoint.
- Every DMML value object used in commands or events has a schema.
- Every DMML domain event has a schema.
Consistency check:
- All
$ref references resolve to defined schemas.
- Path parameters match between URL template and parameter definitions.
- Request body schemas match DMML command attributes.
- Response schemas include all relevant DMML event attributes.
- Error responses cover all documented preconditions and invariants.
- No orphan schemas — every schema in
components/schemas is $ref'd
at least once. If a value object schema exists, it must be used somewhere.
- Nullable propagation — for every DMML attribute marked
nullable: true,
verify the corresponding schema property uses anyOf with type: 'null'
and is NOT in the required list. Check this in event schemas too, not
just entity/resource schemas.
- ErrorResponse structure — verify it uses the nested
error.code /
error.message format, not a flat top-level code / message shape.
- Version — must be
0.1.0.
Naming check:
- Schema names are PascalCase.
- Path segments are kebab-case.
- Property names in schemas are camelCase.
- Operation IDs are camelCase.
Run the validation script:
python scripts/validate_openapi.py <output-file.openapi.yaml>
This checks structural validity: valid YAML, valid OpenAPI 3.1, all refs
resolve, all paths have operations, standard error responses present.
Output
Save the OpenAPI YAML as a single file named after the domain:
<domain-name>.openapi.yaml
For example: joes-pizza.openapi.yaml
The output must be a single valid YAML 1.2 document conforming to OpenAPI
3.1.0. Do not include markdown fences, prose explanations, or any content
outside the YAML.
After saving the file, report a summary:
- Bounded contexts exposed: Which BCs got API sections
- External BCs documented: Which BCs are noted as external
- Endpoints: Total count of POST (commands) and GET (queries) endpoints
- Schemas: Total count of component schemas
- Domain events documented: Count of event schemas
- Error cases: Count of documented error scenarios from invariants
and preconditions
- Draft elements skipped: Any DMML elements at
status: draft that
were included or excluded (and why)
Handling Draft Elements
DMML elements at status: draft represent hypotheses that haven't been
validated. The API Designer should:
- Include draft commands if they have attributes and preconditions —
mark the endpoint with
x-draft: true in the OpenAPI extensions and
add a note in the operation description.
- Include draft read models similarly.
- Exclude draft elements that are too vague (no attributes, placeholder
descriptions) — note them in the summary as "skipped, insufficient detail."
- Never fabricate endpoints for draft elements that lack enough DMML
information to design a meaningful API.
Common Pitfalls
-
Don't invent endpoints the DMML doesn't support. If there's no
command for "update order," don't create a PUT endpoint. The domain model
defines the allowed operations.
-
Don't flatten the domain into CRUD. DDD commands are task-based, not
resource-based. POST /orders/{id}/select-pizza is correct.
PATCH /orders/{id} with a generic body is not.
-
Don't expose internal aggregate structure. The API exposes commands
and responses, not the internal entity graph. Clients don't need to know
about child entities — they interact through the aggregate root's commands.
-
Don't skip error documentation. Every invariant and precondition
should appear as a documented error case on the relevant endpoint. This is
how the API communicates the domain's rules.
-
Don't create endpoints for external BCs. If the DMML marks a BC as
external (no aggregates, generic subdomain), it gets documentation, not
endpoints.
-
Don't use PUT/PATCH for commands. DDD commands are explicit actions
(verbs), not partial updates (nouns). POST is the correct method for
command execution.
Downstream Awareness
The OpenAPI specification you produce feeds into:
- Test generation — Acceptance tests will be generated from the OpenAPI
spec, testing each endpoint against the documented request/response schemas
and error cases.
- Code generation — The API layer (Fastify routes, Zod schemas) will be
generated from the OpenAPI spec. Clear, consistent naming matters.
- Documentation — The spec can be rendered in Swagger UI or Redoc for
human review. Operation descriptions should be readable by domain experts.
- Client generation — API consumers can generate type-safe clients from
the spec.
The more precise the OpenAPI spec, the less manual work the downstream steps
require.