Creating Headless APIs (Part 2)

Use Liferay's REST Builder tool to generate your own Headless APIs

Introduction

In part 1 of this series, we started a project to build our own custom Headless APIs using Liferay's REST Builder tool.

The project was started, four modules were created and I presented the Meta and Reusable Components sections from my OpenAPI Yaml file.

We're going to pick up where we left off and finish presenting the Paths (endpoints) section and then start generating code! Wahoo!

Defining the Paths

The Paths are your REST endpoints in your API.

Defining these are the critical part of making REST endpoints. If you get these wrong and need to refactor or have a future change that breaks the contract, your service consumers are not going to be happy campers.

Honestly it can be easy to get REST endpoint definitions wrong. There is so many examples of bad REST implementations behind us that when we see a right one, we're taken aback.

The paths selected for my resources fall under two forms:

  • /v1.0/vitamins
  • /v1.0/vitamins/{vitaminId}

The first form is for retrieving the collection or creating a new record (depending upon the HTTP method used), the second form is for getting, updating or deleting a specific record given the primary key.

In the definitions below, the responses all point towards the happy-path response. So a getVitamin operation only provides for a successful response with the Vitamin object. Since we are leveraging OpenAPI and especially the Liferay framework around all of the paths, it should be known that there will be a larger set of responses that can include errors or exceptions. Since the framework takes care of all of that, we must only concern ourselves with the successful responses.

Listing All Vitamins

So the first path is the one used to retrieve the list of vitamins/minerals, and it uses paging rather than returning the whole list at one time:

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

So a GET request on /vitamins will return an array of Vitamin objects. On the Swagger side,  we would actually see another component type, PageVitamin, that wraps our array w/ the necessary paging details.

Note: The tags attribute here is important. The value matches the component type that the path either works on or returns. Since my methods all deal with the Vitamin component, all of my tags have the same ["Vitamin"] value. This is absolutely necessary for the code generation.

Like many of the Liferay Headless APIs, we're also going to support searches, filters, controlling the paging, and sorting of the items.

Creating a Vitamin

Using the same path but with a POST method, we can 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 to be created. The response will be the newly created instance.

Retrieving a Vitamin

Our second URL form works for single records. In the first example, a GET request will retrieve a single Vitamin object with the given 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"

Replacing a Vitamin

Using a PUT request, we support replacing a current Vitamin object with the one included in the request body. Any fields not included in the request should be blanked/nulled in the record being 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 includes the Vitamin to replace the existing with, and the response is the new Vitamin object.

Updating a Vitamin

We also allow using a PATCH request to update a current Vitamin. Unlike the PUT which blanks out fields that are not provided, in a PATCH fields that are not part of the request are untouched in the current Vitamin 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 includes the fields of the Vitamin to update, and the response is the new updated Vitamin object.

Deleting a Vitamin

Our last path is the one to delete a vitamin 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 is neither a request body nor a response body with this path.

Reviewing Results

If you define your API using the Swagger Editor, you get a really awesome view of how your services shape up:

Oooh, so colorful, what awesome eye candy!

Seriously though, the editor itself offers some great features when creating your Yaml file including context-sensitive help, immediate feedback for syntax errors, and with the right side view you can see from a 50-foot level if you have your eyes dotted and your tees crossed.

If you do use the Swagger Editor, though, be sure to get your Yaml file from there over to your IDE. We'll be picking up there next.

Best results of the Yaml export happen if you export using the "Resolved Yaml" option in Swagger Hub.

Generating Code, Attempt 1

We're finally ready to invoke our new REST Builder. In the headless-vitamins-impl directory, issue the command:

$ ../../../gradlew buildREST

And, if you're like me, then it ended with a FAIL.

Here's the some of the output the first time I tried 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:
            ^
[...snip...]
> Task :modules:headless-vitamins-impl:buildREST FAILED

Failed is right, but why?

The messages themselves, they are clearly no help. Something is going wrong, but you don't really get a good picture of what it might be.

I figured I'd start by looking again at my OpenAPI Yaml file. Looks fine in the Swagger Editor, no errors there, so it can't be a content issue, can it?

So next I checked the file against the ones Liferay uses for its headless modules, and I found a bunch of differences. I've actually gone back into the blog here and fixed them so you won't see these errors that I dumped...

Long story short, you can't just use any Yaml format you want, even the format from the Swagger Editor won't make it through the buildREST command.

Here's a brief list of differences that can trip you up:

  • The Yaml for Liferay's headless-delivery API online uses a lot of responses as "default", but this is not accepted by REST Builder, you have to actually use real response codes. The Liferay Yaml files in Github use the real response codes.
  • Descriptions in the components section do not need to be quoted, but apparently they do need to be quoted in the paths section.
  • Descriptions, etc. can wrap in online Yaml, but REST Builder appears to want everything on one long line.
  • Paths must be quoted.
  • Tags are formatted differently, REST Builder expects a format such as tags: ["Vitamins"] instead of the online version.
  • The /v1.0 portion of the URL you see on Swagger? That should not be included in your path definition.

There are likely other things which differ that I didn't catch. If you end up getting errors like the ones I listed above, start comparing your file to one of Liferay's such as https://github.com/liferay/liferay-portal/blob/master/modules/apps/headless/headless-delivery/headless-delivery-impl/rest-openapi.yaml and verify that you quote the same things Liferay does, use similar formatting, etc.

Generating Code, Attempt 2

Although I'm calling this attempt 2, it was probably more like attempt 7 or something. I would get some kind of error from REST Builder, then I'd compare my file to Liferay's, find something else that was slightly different in my own file, then I'd tweak it and run REST Builder again. Eventually there were no more build errors, and that's what I'm now calling attempt 2...

Now my Yaml file is formatted exactly as Liferay's files are, so I'm confident that now I can generate some code!

$ ../../../gradlew buildREST

This time we get some good news:

> 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

So this ends up being a good stopping point for this part of the blog series...

Conclusion

In part 1 of the blog series, we created a project for our new Headless APIs, plus we also created our config yaml file and started our OpenAPI yaml file by defining the object types we were going to be passing around.

In this part, we picked up where we left off by adding in all of the paths (endpoints) our REST application is going to expose. We tried to do a build only to stumble on some fairly common pitfalls we will encounter when writing the OpenAPI yaml files and worked out how to use Liferay's files as examples when we encounter buildREST task errors.

We finished this part by successfully invoking buildREST to generate code for our new Headless API.

In the next part, we'll dive into the generated code and see where we need to start adding some logic.

See you soon!

https://github.com/dnebing/vitamins

 

4
Blogs

Thanks, Dave! I wasn't having much luck getting mine to build until I came across these blog posts. Very well explained.

Hi David thanks for block it helps me to learn Open Api in liferay.

but I am getting error when i am trying to invoke get api with this /v1.0/vitamins/{vitaminId}.

i am getting this error " HV000030: No validator could be found for constraint 'javax.validation.constraints.NotNull' validating type 'java.lang.String'. Check configuration for" can you help me on this please 

Thanks for the instructions, I am using the CE version of Liferay 7.4.2 GA3, for the GraphQL multipart upload, to enable it in GraphQL, we need to add below lines to the web.xml, but in 7.4.2 GA3, the web.xml doesn't have the servlet configuration, I saw there was one in the shielded-container-web.xml, but when i config the <multipart-config> in the servlet part, it doesn't work, could you please help to let me know how to enable uploading files for GraphQL in CE version of 7.4.2 GA3?

<servlet>         <servlet-name>Module Framework Servlet</servlet-name>         <servlet-class>com.liferay.portal.module.framework.ModuleFrameworkServletAdapter</servlet-class>         <load-on-startup>1</load-on-startup>         <async-supported>true</async-supported>         <multipart-config>             <location>/temp</location>             <max-file-size>20848820</max-file-size>             <max-request-size>418018841</max-request-size>             <file-size-threshold>1048576</file-size-threshold>         </multipart-config>     </servlet>