In Part 1 of this series (https://liferay.dev/blogs/-/blogs/creating-headless-apis-part-1), you'll use Liferay's REST Builder tools to create your own custom headless API. We have started a project to build. We started the project and created four modules to introduce you to the Meta and Reusable Components sections of your OpenAPI Yaml file.
In this article, which is a continuation of the previous article, we will move on to the Paths section to get into code generation.
The path is the REST endpoint of the API. These definitions are an important part of creating a REST endpoint. Any mistakes in this process can lead to refactoring and catastrophic changes in the future, which can be inconvenient for service users. In fact, REST endpoints tend to have improper definitions. The bad practices of REST implementations are so numerous that you'll be amazed when you find the right one.
The path chosen for the resource has two forms:
The first format is to retrieve a collection or create a new record (according to the HTTP method used), and the second format is to retrieve, update, and delete a specific record with a primary key.
In the definition below, all responses refer to happy path responses. So getVitamin
only provides a successful response on a Vitamin object. It's important to keep in mind that because we leverage OpenAPI, especially the Liferay framework, in every path, we have a large set of responses that can contain errors and exceptions. The framework handles all of them, so you only need to pay attention to successful responses.
Therefore, the first path is the path used to get the list of vitamins / minerals and uses paging instead of returning the entire list at once.
paths:
"/vitamins":
get:
tags: ["Vitamin"]
description: Retrieves the list of vitamins and minerals. Results can be paginated, filtered, searched, and sorted.
parameters:
- in: query
name: filter
schema:
type: string
- in: query
name: page
schema:
type: integer
- in: query
name: pageSize
schema:
type: integer
- in: query
name: search
schema:
type: string
- in: query
name: sort
schema:
type: string
responses:
200:
description: ""
content:
application/json:
schema:
items:
$ref: "#/components/schemas/Vitamin"
type: array
application/xml:
schema:
items:
$ref: "#/components/schemas/Vitamin"
type: array
A GET request for / vitamins
returns an array of Vitamin objects. On the Swagger side, you'll actually see another component type called PageVitamin
that wraps the array with the required paging details.
** Note: The tags attribute here is important. This value matches the component type that the path operates on or the component type that is returned. All my methods work with Vitamin components, so all tag values are the same [" Vitamin "]
. This is absolutely necessary for code generation. ** **
Like many Liferay Headless APIs, it also supports search, filtering, paging control, and item sorting.
You can use the POST method on the same path to create a new vitamin / mineral object.
post:
tags: ["Vitamin"]
description: Create a new vitamin/mineral.
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/Vitamin"
application/xml:
schema:
$ref: "#/components/schemas/Vitamin"
responses:
200:
description: ""
content:
application/json:
schema:
$ref: "#/components/schemas/Vitamin"
application/xml:
schema:
$ref: "#/components/schemas/Vitamin"
The body of the request will be the vitamin object created and the response will be the newly created instance.
The second URL form works for a single record. In the first example, the GET request gets a single Vitamin object with the specified vitaminId
.
"/vitamins/{vitaminId}":
get:
tags: ["Vitamin"]
description: Retrieves the vitamin/mineral via its ID.
parameters:
- name: vitaminId
in: path
required: true
schema:
type: string
responses:
200:
description: ""
content:
application/json:
schema:
$ref: "#/components/schemas/Vitamin"
application/xml:
schema:
$ref: "#/components/schemas/Vitamin"
You can use a PUT request to replace the current vitamin object with the object contained in the request body. Fields not included in the request must be blank or null
in the record to be replaced.
put:
tags: ["Vitamin"]
description: Replaces the vitamin/mineral with the information sent in the request body. Any missing fields are deleted, unless they are required.
parameters:
- name: vitaminId
in: path
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/Vitamin"
application/xml:
schema:
$ref: "#/components/schemas/Vitamin"
responses:
200:
description: Default Response
content:
application/json:
schema:
$ref: "#/components/schemas/Vitamin"
application/xml:
schema:
$ref: "#/components/schemas/Vitamin"
The request contains a vitamin that replaces the existing one, and the response is a new vitamin object.
You can also use the PATCH request to update your current vitamins. Unlike PUT, which blanks out fields that are not provided, fields that are not part of the request in PATCH are not modified in the object.
patch:
tags: ["Vitamin"]
description: Replaces the vitamin/mineral with the information sent in the request body. Any missing fields are deleted, unless they are required.
parameters:
- name: vitaminId
in: path
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/Vitamin"
application/xml:
schema:
$ref: "#/components/schemas/Vitamin"
responses:
200:
description: ""
content:
application/json:
schema:
$ref: "#/components/schemas/Vitamin"
application/xml:
schema:
$ref: "#/components/schemas/Vitamin"
The request will contain a field of vitamins to update and the response will be the updated vitamin object.
The last is the path to remove vitamins using a DELETE request.
delete:
tags: ["Vitamin"]
description: Deletes the vitamin/mineral and returns a 204 if the operation succeeds.
parameters:
- name: vitaminId
in: path
required: true
schema:
type: string
responses:
204:
description: ""
content:
application/json: {}
There are no request or response bodies in this path.
You can use the Swagger Editor to define your API for a clear review of how your service works.
As you can see in the figure above, it's easy to understand visually!
When creating a Yaml file, the editor itself provides great features such as context-sensitive help and immediate feedback on syntax errors, which can be very helpful in understanding the API configuration.
If you use Swagger Editor, don't forget to move the Yaml file to the IDE.
You are ready to call the new REST Builder. Run the following command in the headless-vitamins-impl
directory.
$ ../../../gradlew buildREST
Like me, you may have failed. Here is a portion of the output when you first run buildREST
:
Exception in thread "main" Cannot create property=paths for JavaBean=com.liferay.portal.vulcan.yaml.openapi.OpenAPIYAML@1e730495
in 'string', line 1, column 1:
openapi: 3.0.1
^
Cannot create property=get for JavaBean=com.liferay.portal.vulcan.yaml.openapi.PathItem@23f7d05d
in 'string', line 8, column 5:
get:
^
Cannot create property=responses for JavaBean=com.liferay.portal.vulcan.yaml.openapi.Get@23986957
in 'string', line 9, column 7:
tags:
^
For input string: "default"
in 'string', line 36, column 9:
default:
^
[...abridgement...]
> Task :modules:headless-vitamins-impl:buildREST FAILED
Why did you fail? The message lacks specificity and the cause cannot be accurately determined.
Let's take a look at the OpenAPI Yaml file again. It seems that it is not a content problem because it is displayed without problems in Swagger Editor.
Next, when I compared this file to what Liferay uses for headless modules, there were many differences. I've already fixed the blog, so I don't see the error. To put it plainly, the simple Yaml format does not give the expected results with the build REST
command, even in the Swagger Editor format.
Below is a brief introduction to the differences:
The Yaml for Liferay headless-delivery API uses many responses as the "default", which is not accepted by REST Builder and requires the use of the actual response code. The Liferay Yaml file on Github uses the actual response code.
The description of the component section does not need to be quoted, but the path section does.
--Description etc. can be wrapped in online Yaml, but REST Builder tries to put everything together in one line.
--The path must be enclosed in quotation marks.
--Tags are formatted in a different format. REST Builder expects a format such as tags: ["Vitamins "]
rather than the online version.
--The / v1.0
part of the URL displayed in Swagger should not be included in the path definition.
There may be other differences that I haven't noticed. If you get an error like the one above, make the file [Liferay Official](https://github.com/liferay/liferay-portal/blob/master/modules/apps/headless/headless-delivery/headless- Compare it with delivery-impl / rest-openapi.yaml) and check how to use quotation and whether it conforms to the same format.
After this trial and error, my Yaml file was formatted to follow Liferay's, and the code was successfully generated.
$ ../../../gradlew buildREST
Results on success:
> Task :modules:headless-vitamins-impl:buildREST
Writing vitamins/modules/headless-vitamins/headless-vitamins-impl/src/main/java/com/dnebinger/headless/vitamins/internal/jaxrs/application/HeadlessVitaminsApplication.java
Writing vitamins/modules/headless-vitamins/headless-vitamins-client/src/main/java/com/dnebinger/headless/vitamins/client/json/BaseJSONParser.java
Writing vitamins/modules/headless-vitamins/headless-vitamins-client/src/main/java/com/dnebinger/headless/vitamins/client/http/HttpInvoker.java
Writing vitamins/modules/headless-vitamins/headless-vitamins-client/src/main/java/com/dnebinger/headless/vitamins/client/pagination/Page.java
Writing vitamins/modules/headless-vitamins/headless-vitamins-client/src/main/java/com/dnebinger/headless/vitamins/client/pagination/Pagination.java
Writing vitamins/modules/headless-vitamins/headless-vitamins-client/src/main/java/com/dnebinger/headless/vitamins/client/function/UnsafeSupplier.java
Writing vitamins/modules/headless-vitamins/headless-vitamins-impl/rest-openapi.yaml
Writing vitamins/modules/headless-vitamins/headless-vitamins-impl/src/main/java/com/dnebinger/headless/vitamins/internal/graphql/mutation/v1_0/Mutation.java
Writing vitamins/modules/headless-vitamins/headless-vitamins-impl/src/main/java/com/dnebinger/headless/vitamins/internal/graphql/query/v1_0/Query.java
Writing vitamins/modules/headless-vitamins/headless-vitamins-impl/src/main/java/com/dnebinger/headless/vitamins/internal/graphql/servlet/v1_0/ServletDataImpl.java
Writing vitamins/modules/headless-vitamins/headless-vitamins-impl/src/main/java/com/dnebinger/headless/vitamins/internal/resource/v1_0/OpenAPIResourceImpl.java
Writing vitamins/modules/headless-vitamins/headless-vitamins-api/src/main/java/com/dnebinger/headless/vitamins/dto/v1_0/Vitamin.java
Writing vitamins/modules/headless-vitamins/headless-vitamins-client/src/main/java/com/dnebinger/headless/vitamins/client/dto/v1_0/Vitamin.java
Writing vitamins/modules/headless-vitamins/headless-vitamins-client/src/main/java/com/dnebinger/headless/vitamins/client/serdes/v1_0/VitaminSerDes.java
Writing vitamins/modules/headless-vitamins/headless-vitamins-api/src/main/java/com/dnebinger/headless/vitamins/dto/v1_0/Creator.java
Writing vitamins/modules/headless-vitamins/headless-vitamins-client/src/main/java/com/dnebinger/headless/vitamins/client/dto/v1_0/Creator.java
Writing vitamins/modules/headless-vitamins/headless-vitamins-client/src/main/java/com/dnebinger/headless/vitamins/client/serdes/v1_0/CreatorSerDes.java
Writing vitamins/modules/headless-vitamins/headless-vitamins-impl/src/main/java/com/dnebinger/headless/vitamins/internal/resource/v1_0/BaseVitaminResourceImpl.java
Writing vitamins/modules/headless-vitamins/headless-vitamins-impl/src/main/resources/OSGI-INF/liferay/rest/v1_0/vitamin.properties
Writing vitamins/modules/headless-vitamins/headless-vitamins-api/src/main/java/com/dnebinger/headless/vitamins/resource/v1_0/VitaminResource.java
Writing vitamins/modules/headless-vitamins/headless-vitamins-impl/src/main/java/com/dnebinger/headless/vitamins/internal/resource/v1_0/VitaminResourceImpl.java
Writing vitamins/modules/headless-vitamins/headless-vitamins-client/src/main/java/com/dnebinger/headless/vitamins/client/resource/v1_0/VitaminResource.java
Writing vitamins/modules/headless-vitamins/headless-vitamins-test/src/testIntegration/java/com/dnebinger/headless/vitamins/resource/v1_0/test/BaseVitaminResourceTestCase.java
Writing vitamins/modules/headless-vitamins/headless-vitamins-test/src/testIntegration/java/com/dnebinger/headless/vitamins/resource/v1_0/test/VitaminResourceTest.java
BUILD SUCCESSFUL in 2s
1 actionable task: 1 executed
Therefore, this is the end of Part 2 of this blog series.
In [Part 1] of the blog series (https://qiita.com/TsubasaHomma/items/df3f93c8c54daa9a058b), we will work on creating a new headless API project, creating a configuration yaml file, defining object types and opening API yaml files. I did.
As a continuation of this part, we have added all the paths (endpoints) of the REST application. We touched on some common points you might face when creating an OpenAPI yaml file, and how to compare Liferay files as an example in the event of a buildREST
task error.
I ended this part by successfully calling buildREST
to generate the code for the new headless API.
In the next part (https://qiita.com/TsubasaHomma/items/101f9e3cba6334f7da01), we'll dig into the generated code to see where we need to start adding logic. See you again! https://github.com/dnebing/vitamins
Recommended Posts