Blogs
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.
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.
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
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