Skip to content

Exposing a REST API for your Domain Services

Section titled “Exposing a REST API for your Domain Services”

Because writing YAML and dealing with boilerplate code is no fun…

In the following sections, we’ll explore how to use ZenWave Domain Language (ZDL) as an Interface Definition Language (IDL) for OpenAPI, leveraging ZenWave SDK to generate complete OpenAPI v3 specifications from your domain models.

You can then use these generated OpenAPI definitions to build various aspects of your application using ZenWave API-First tooling.

ZenWave Domain Language as Interface Definition Language (IDL) for OpenAPI

Section titled “ZenWave Domain Language as Interface Definition Language (IDL) for OpenAPI”

Because writing YAML by hand is no fun… You can use ZDL models as Interface Definition Language (IDL) to generate OpenAPI v3 with ZenWave SDK ZDLToOpenAPIPlugin.

This approach allows you to:

  • Define APIs using a compact, developer-friendly syntax
  • Maintain consistency between your domain model and API contracts
  • Consisten style between your different APIs: naming, pagination, status codes, error handling…
  • Reduce manual YAML writing and potential errors

ZDL has evolved to support defining OpenAPI APIs almost completely, including file uploads and downloads, status codes, and more.

⚠️ Remember: Once generated, the OpenAPI definition becomes the source of truth for your REST API. ZDL serves as a starting point for rapid API design.

First step is to decorate your services with @rest to indicate that they will be exposed via REST API and define the base path for all operations in this service.

@rest("/customers")
service CustomerService for (Customer) {
//...
}

Now you can use ZDLToOpenAPIPlugin to generate an OpenAPI definition from your ZDL model:

ZDLToOpenAPIPlugin in zenwave-scripts.zw
ZDLToOpenAPIPlugin {
idType integer
idTypeFormat int64
targetFile "src/main/resources/public/apis/openapi.yml"
}

Navigate to source

Remember that this is equivalent to running:

ZDLToOpenAPIPlugin using JBang CLI
OpenAPI Customer Service Outline

Decorating your Service Methods with @get, @post, @put, @delete, @patch

Section titled “Decorating your Service Methods with @get, @post, @put, @delete, @patch”

All @get, @post, @put, @delete, @patch support the following syntax:

  • @get("/somepath/{somePathParam}"): simpler form, shorthand for @get({path: "/somepath/{somePathParam}"})
  • @get({path: "/somepath", status: 200, params: {search: String}, operationId: "someOperationId"}):
    • path: the path for this operation, relative to the service base path.
    • status: the default status code for this operation, this is usefull for @post operations that return 200 instead of the default 201.
    • params: a map of parameters and their types, they will match path params o query params if no path param was matched.
    • operationId: the operationId for this operation, by default it will use the service command name.

When using the `ZDLToOpenAPIPlugin`, the following

decorated service:
@rest("/customers")
service CustomerService for (Customer) {
@post
createCustomer(Customer) Customer withEvents CustomerEvent
@get("/{id}")
getCustomer(id) Customer?
@put("/{id}")
updateCustomer(id, Customer) Customer? withEvents CustomerEvent
@delete("/{id}")
deleteCustomer(id) withEvents CustomerDeletedEvent
@post(path: "/search", status: 200)
@paginated
searchCustomers(CustomerSearchCriteria) Customer[]
}

Navigate to source

would produce this openapi definition:

src/main/resources/public/apis/openapi.yml

Navigate to source

As you would expect, @get will generate a GET operations, with path and query parameters defined in the path and params properties, with their corresponding data types. It will return an status code 200 by default, but you can change it using the status property.

When using the `ZDLToOpenAPIPlugin`, the following

this service command:
@get("/{id}")
getCustomer(id) Customer?

Navigate to source

would produce this:

src/main/resources/public/apis/openapi.yml
/customers/{id}:
get:
operationId: getCustomer
description: "getCustomer"
tags: [Customer]
parameters:
- name: "id"
in: path
required: true
schema:
type: integer
format: int64
responses:
"200":
description: "OK"
content:
application/json:
schema:
$ref: "#/components/schemas/Customer"

Navigate to source

If the method command has a command payload, it will be expanded as path or query parameters.

When using the `ZDLToOpenAPIPlugin`, the following

this service command:
@get({ path: "/{name}/patient/{patientId}/questions", params: { lang: String } })
getSurveyAndQuestionsForPatient(SurveyByNameAndPatient) SurveyAndQuestions

Navigate to source

would produce this:

src/main/resources/public/apis/surveys-public-openapi.yml
/public/surveys/{name}/patient/{patientId}/questions:
get:
operationId: getSurveyAndQuestionsForPatient
description: "getSurveyAndQuestionsForPatient"
tags: [Surveys]
parameters:
- name: "name"
in: path
required: true
schema:
type: string
- name: "patientId"
in: path
required: true
schema:
type: integer
format: int64
- name: "lang"
in: query
schema:
type: string
responses:
"200":
description: "OK"
content:
application/json:
schema:
$ref: "#/components/schemas/SurveyAndQuestions"

Navigate to source

Notice how path and query parameter data types are inferred:

  • name path param is inferred from SurveyByNameAndPatient.name field type String.
  • patientId path param is inferred from SurveyByNameAndPatient.patientId field type Long.
  • lang is added as query parameter because it is not a path parameter.

Posts are used to create new resources. The default status code is 201, if you need to change it, because you are implementing a search operation for example, you can use the status property.

When using the `ZDLToOpenAPIPlugin`, the following

this service command:
@post
createCustomer(Customer) Customer withEvents CustomerEvent

Navigate to source

would produce this:

src/main/resources/public/apis/openapi.yml
/customers:
post:
operationId: createCustomer
description: "createCustomer"
tags: [Customer]
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/Customer"
responses:
"201":
description: "OK"
content:
application/json:
schema:
$ref: "#/components/schemas/Customer"

Navigate to source

You can also use @post to implement search operations, in this case you should use status: 200 to indicate that the operation returns a list of resources:

When using the `ZDLToOpenAPIPlugin`, the following

this service command:
@post(path: "/search", status: 200) @paginated
searchUsers(SearchCriteria) User[]

Navigate to source

would produce this:

src/main/resources/public/apis/openapi.yml

Navigate to source

When using the `ZDLToOpenAPIPlugin`, the following

this service command:
@put("/{id}")
updateCustomer(id, Customer) Customer? withEvents CustomerEvent

Navigate to source

would produce this:

src/main/resources/public/apis/openapi.yml
put:
operationId: updateCustomer
description: "updateCustomer"
tags: [Customer]
parameters:
- name: "id"
in: path
required: true
schema:
type: integer
format: int64
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/Customer"
responses:
"200":
description: "OK"
content:
application/json:
schema:
$ref: "#/components/schemas/Customer"

Navigate to source

Delete operations would return a 204 status code by default, but you can change it using the status property.

When using the `ZDLToOpenAPIPlugin`, the following

this service command:
@delete("/{id}")
deleteCustomer(id) withEvents CustomerDeletedEvent

Navigate to source

would produce this:

src/main/resources/public/apis/openapi.yml
delete:
operationId: deleteCustomer
description: "deleteCustomer"
tags: [Customer]
parameters:
- name: "id"
in: path
required: true
schema:
type: integer
format: int64
responses:
"204":
description: "OK"

Navigate to source

When using the `ZDLToOpenAPIPlugin`, the following

this service command:
@patch({ path: "/{surveyId}/patient/{patientId}/answers/{date}", params: { surveyId: Long } })
updateSurveyAnswers(@natural id, SurveyAnswers) SurveyAnswers?

Navigate to source

would produce this:

src/main/resources/public/apis/surveys-public-openapi.yml
patch:
operationId: updateSurveyAnswers
description: "updateSurveyAnswers"
tags: [Surveys]
parameters:
- name: "surveyId"
in: path
required: true
schema:
type: integer
format: int64
- name: "patientId"
in: path
required: true
schema:
type: integer
format: int64
- name: "date"
in: path
required: true
schema:
type: string
format: date
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/SurveyAnswersPatch"
responses:
"200":
description: "OK"
content:
application/json:
schema:
$ref: "#/components/schemas/SurveyAnswers"

Navigate to source

#/components/schemas/SurveyAnswersPatch
SurveyAnswersPatch:
allOf:
- $ref: "#/components/schemas/SurveyAnswers"

Navigate to source

When configuring `schemaMappings` in `openapi-generator-maven-plugin` like this:

this service command:
<schemaMappings>
SurveyAnswersPatch=java.util.Map
</schemaMappings>

Navigate to source

will generate a java method with `Map input` as parameter:

src/main/resources/public/apis/surveys-public-openapi.yml
@Override
public ResponseEntity<SurveyAnswersDTO> updateSurveyAnswers(
Long surveyId, Long patientId, LocalDate date, Map input) {

Navigate to source

Because Map input contains only the properties that where actually send by the client, you can easily implement a partial update.

ZDLToOpenAPIPlugin supports the following pagination style out of the box when using @paginated annotation:

When using the `ZDLToOpenAPIPlugin`, the following

this `@paginated` service command:
@post(path: "/search", status: 200)
@paginated
searchCustomers(CustomerSearchCriteria) Customer[]

Navigate to source

would produce this:

src/main/resources/public/apis/openapi.yml
/customers/search:
post:
operationId: searchCustomers
description: "searchCustomers"
tags: [Customer]
parameters:
- $ref: "#/components/parameters/page"
- $ref: "#/components/parameters/limit"
- $ref: "#/components/parameters/sort"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CustomerSearchCriteria"
responses:
"200":
description: "OK"
content:
application/json:
schema:
$ref: "#/components/schemas/CustomerPaginated"

Navigate to source

With the CustomerPaginated schema defined as:

#/components/schemas/CustomerPaginated
CustomerPaginated:
allOf:
- $ref: "#/components/schemas/Page"
- properties:
content:
type: "array"
items:
$ref: "#/components/schemas/Customer"

Navigate to source

And the following components definition:

Supporting Schemas for Pagination

Navigate to source

ZDLToOpenAPIPlugin supports generating OpenAPI definitions for file upload endpoints using the @fileupload("fieldName") annotation being fieldName the name of the field in the request body that contains the file data.

When using the `ZDLToOpenAPIPlugin`, the following

this `@fileupload` service command:
@post({path: "/upload", status: 201})
@fileupload("file")
uploadDocument(DocumentInfo) DocumentInfo

Navigate to source

would produce this:

src/main/resources/public/apis/openapi.yml
/documents/upload:
post:
operationId: "uploadDocument"
description: "uploadDocument"
tags:
- "Document"
parameters:
- name: "uuid"
in: "query"
required: false
schema:
type: "string"
- name: "tags"
in: "query"
required: false
style: "form"
explode: true
schema:
type: "array"
items:
type: "string"
requestBody:
required: true
content:
multipart/form-data:
schema:
type: "object"
required:
- "file"
properties:
file:
type: "string"
format: "binary"
responses:
"201":
description: "OK"
content:
application/json:
schema:
$ref: "#/components/schemas/DocumentInfo"

Navigate to source

And the corresponding SpringMVC Controller:

DocumentApiController.java#uploadDocument

Navigate to source

ZDLToOpenAPIPlugin supports generating OpenAPI definitions for file upload endpoints using the @filedownload("javaFieldName") annotation, being javaFieldName the name of the field in java model that contains the binary data to be downloaded.

When using the `ZDLToOpenAPIPlugin`, the following

this `@filedownload` service command:
@get({path: "/{id}", params: {preview: boolean} })
@filedownload("documentData.data")
downloadDocument(id) DocumentInfo

Navigate to source

would produce this:

src/main/resources/public/apis/openapi.yml
/documents/{id}:
get:
operationId: "downloadDocument"
description: "downloadDocument"
tags:
- "Document"
parameters:
- name: "id"
in: "path"
required: true
schema:
type: "integer"
format: "int64"
- name: "preview"
in: "query"
schema:
type: "boolean"
responses:
"200":
description: "OK"
headers:
Content-Disposition:
description: "Controls file download behavior. Values: 'inline' (display\
\ in browser) or 'attachment; filename=example.pdf' (download file)"
schema:
type: "string"
content:
'*/*':
schema:
type: "string"
format: "binary"

Navigate to source

And the corresponding SpringMVC Controller:

DocumentApiController.java#uploadDocument

Navigate to source