How to create your own headless API using Liferay's REST Builder (Part 2)

Introduction

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.

Path definition

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.

List of all vitamins

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.

Vitamin production

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.

Get vitamins

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"

Vitamin exchange

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.

Vitamin renewal

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.

Remove vitamins

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.

Check the result

You can use the Swagger Editor to define your API for a clear review of how your service works.

Swagger Editorの結果確認画面

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.

Code generation trial part 1

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.

Code generation trial part 2

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.

Summary

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

How to create your own headless API using Liferay's REST Builder (Part 2)
How to create your own headless API using Liferay's REST Builder (Part 4)
How to create your own headless API using Liferay's REST Builder (Part 1)
How to create your own Controller corresponding to / error with Spring Boot
How to create your own annotation in Java and get the value
Let's create a REST API using WildFly Swarm.
[Rails] How to create a graph using lazy_high_charts
How to implement optimistic locking in REST API
How to create hierarchical category data using ancestry
[Java] How to operate List using Stream API
How to create docker-compose
[Forge] How to register your own Entity and Entity Render in 1.13.2
How to create a placeholder part to use in the IN clause
How to deploy jQuery in your Rails app using Webpacker
How to create a service builder portlet in Liferay 7 / DXP
How to play MIDI files using the Java Sound API
How to create an application
How to use Chain API
How to use @Builder (Lombok)
Create your own Java annotations
Introduction to EHRbase 2-REST API
How to create a method
How to authorize using graphql-ruby
How to create a jar file or war file using the jar command
[Rails 6] How to create a dynamic form input screen using cocoon
Easy way to create a mapping class when using the API