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.
Decorating your Services with @rest
Section titled “Decorating your Services with @rest”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:
Remember that this is equivalent to running:
ZDLToOpenAPIPlugin using JBang CLI
jbang zw -p ZDLToOpenAPIPlugin \ zdlFile=zenwave-model.zdl \ idType=integer \ idTypeFormat=int64 \ targetFolder=. \ targetFile=src/main/resources/public/apis/openapi.yml
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@postoperations that return200instead of the default201.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[]}would produce this openapi definition:
src/main/resources/public/apis/openapi.yml
openapi: 3.0.1info: title: "ZenWave Customer JPA Example" version: 0.0.1 description: "ZenWave Customer JPA Example" contact: email: email@domain.comservers: - description: localhost url: http://localhost:8080/api - description: custom url: "{protocol}://{server}/{path}" variables: protocol: enum: ['http', 'https'] default: 'http' server: default: 'localhost:8080' path: default: 'api'tags: - name: "Default" - name: "Customer"
paths: /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" /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" 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" delete: operationId: deleteCustomer description: "deleteCustomer" tags: [Customer] parameters: - name: "id" in: path required: true schema: type: integer format: int64 responses: "204": description: "OK" /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"
components: schemas: Customer: type: "object" x-business-entity: "Customer" required: - "name" - "email" properties: id: type: "integer" format: "int64" readOnly: true version: type: "integer" default: "null" description: "Version of the document (required in PUT for concurrency control,\ \ should be null in POSTs)." name: type: "string" maxLength: 254 description: "Customer name" email: type: "string" maxLength: 254 pattern: "^[^@]+@[^\\s@]+\\.[^\\s@]+$" description: "" addresses: type: "array" items: $ref: "#/components/schemas/Address" minLength: 1 maxLength: 5 paymentMethods: type: "array" items: $ref: "#/components/schemas/PaymentMethod" CustomerPaginated: allOf: - $ref: "#/components/schemas/Page" - properties: content: type: "array" items: $ref: "#/components/schemas/Customer" CustomerSearchCriteria: type: "object" x-business-entity: "CustomerSearchCriteria" properties: name: type: "string" email: type: "string" city: type: "string" state: type: "string" PaymentMethodType: type: "string" x-business-entity: "PaymentMethodType" enum: - "VISA" - "MASTERCARD" Address: type: "object" x-business-entity: "Address" required: - "street" - "city" properties: street: type: "string" maxLength: 254 city: type: "string" maxLength: 254 PaymentMethod: type: "object" x-business-entity: "PaymentMethod" required: - "type" - "cardNumber" properties: id: type: "integer" format: "int64" readOnly: true version: type: "integer" default: "null" description: "Version of the document (required in PUT for concurrency control,\ \ should be null in POSTs)." type: $ref: "#/components/schemas/PaymentMethodType" cardNumber: type: "string"
Page: type: object required: - "content" - "totalElements" - "totalPages" - "size" - "number" properties: number: type: integer minimum: 0 numberOfElements: type: integer minimum: 0 size: type: integer minimum: 0 maximum: 200 multipleOf: 25 totalElements: type: integer totalPages: type: integer
parameters: page: name: page in: query description: The number of results page schema: type: integer format: int32 default: 0 limit: name: limit in: query description: The number of results in a single page schema: type: integer format: int32 default: 20 sort: name: sort in: query description: Sort results by field name and direction (asc or desc) schema: type: array items: type: string
securitySchemes: basicAuth: # <-- arbitrary name for the security scheme type: http scheme: basic bearerAuth: # <-- arbitrary name for the security scheme type: http scheme: bearer bearerFormat: JWT # optional, arbitrary value for documentation purposessecurity: - basicAuth: [] # <-- use the same name here - bearerAuth: [] # <-- use the same name hereUsing @get
Section titled “Using @get”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
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"If the method command has a command payload, it will be expanded as path or query parameters.
When using the `ZDLToOpenAPIPlugin`, the following
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"Notice how path and query parameter data types are inferred:
namepath param is inferred fromSurveyByNameAndPatient.namefield typeString.patientIdpath param is inferred fromSurveyByNameAndPatient.patientIdfield typeLong.langis added as query parameter because it is not a path parameter.
Using @post for Creating Resources
Section titled “Using @post for Creating Resources”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
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"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
would produce this:
src/main/resources/public/apis/openapi.yml
/users/search: post: operationId: "searchUsers" description: "searchUsers" tags: - "User" parameters: - $ref: "#/components/parameters/page" - $ref: "#/components/parameters/limit" - $ref: "#/components/parameters/sort" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/SearchCriteria" responses: "200": description: "OK" content: application/json: schema: $ref: "#/components/schemas/UserPaginated"Using @put for Updating Resources
Section titled “Using @put for Updating Resources”When using the `ZDLToOpenAPIPlugin`, the following
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"Using @delete for Deleting Resources
Section titled “Using @delete for Deleting Resources”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
would produce this:
Using @patch for Partial Updates
Section titled “Using @patch for Partial Updates”When using the `ZDLToOpenAPIPlugin`, the following
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"When configuring `schemaMappings` in `openapi-generator-maven-plugin` like this:
will generate a java method with `Map input` as parameter:
Because Map input contains only the properties that where actually send by the client, you can easily implement a partial update.
Returning Paginated Lists with @paginated
Section titled “Returning Paginated Lists with @paginated”ZDLToOpenAPIPlugin supports the following pagination style out of the box when using @paginated annotation:
When using the `ZDLToOpenAPIPlugin`, the following
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"With the CustomerPaginated schema defined as:
And the following components definition:
Supporting Schemas for Pagination
Page: type: object required: - "content" - "totalElements" - "totalPages" - "size" - "number" properties: number: type: integer minimum: 0 numberOfElements: type: integer minimum: 0 size: type: integer minimum: 0 maximum: 200 multipleOf: 25 totalElements: type: integer totalPages: type: integer
parameters: page: name: page in: query description: The number of results page schema: type: integer format: int32 default: 0 limit: name: limit in: query description: The number of results in a single page schema: type: integer format: int32 default: 20 sort: name: sort in: query description: Sort results by field name and direction (asc or desc) schema: type: array items: type: stringFile Uploads
Section titled “File Uploads”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
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"And the corresponding SpringMVC Controller:
DocumentApiController.java#uploadDocument
public ResponseEntity<DocumentInfoDTO> uploadDocument( String uuid, List<String> tags, org.springframework.web.multipart.MultipartFile file) { log.debug("REST request to uploadDocument: {}, {}, {}", file, uuid, tags); var input = mapper.asDocumentInfo(file, uuid, tags); var documentInfo = documentService.uploadDocument(input); DocumentInfoDTO responseDTO = mapper.asDocumentInfoDTO(documentInfo); return ResponseEntity.status(201).body(responseDTO); }File Downloads
Section titled “File Downloads”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
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"And the corresponding SpringMVC Controller:
DocumentApiController.java#uploadDocument
@Override public ResponseEntity<Resource> downloadDocument(Long id, Boolean preview) { log.debug("REST request to downloadDocument: {}, {}", id, preview); var documentInfo = documentService.downloadDocument(id); byte[] bytes = documentInfo.getDocumentData().getData(); ByteArrayResource resource = new ByteArrayResource(bytes); return ResponseEntity.status(200) .header("Content-Disposition", "inline") // or attachment; filename=example.pdf .contentType(MediaType.APPLICATION_OCTET_STREAM) // TODO: set content type .body(resource); }