Blogs
Use Liferay's REST Builder tool to generate your own Headless APIs.
Introduction
So recently I have been working with Liferay's new Headless APIs...
I have a nice React-based SPA leveraging the new headless-delivery and headless-user-admin modules. I'm going to release everything eventually, but some parts just aren't ready yet.
The first part is ready, though: using the new REST Builder tool to create your own Headless APIs.
I know what you're thinking...
Why should I use REST Builder instead of just rolling my own REST using good ole JAX-RS?
That's a really good question, I'm glad you were thinking about it...
Liferay's REST Builder does more than just build a simple Application that exposes endpoints. It certainly does those things, but it also adds to the mix:
- Integration with Liferay's authentication pipelines.
- Integration with Liferay's CORS handling.
- Integration with Liferay's Headless facilities to support search, filtering, paging, etc.
- Ability to generate JSON or XML on caller's request.
- Coming Soon - Integration with Liferay's GraphQL endpoint, so the REST Builder APIs will be available via GraphQL w/o any changes on your part.
- Provide consistency to your mobile and/or SPA application developers.
So these points may not apply to you. If they don't, you might want to go your own way, especially if it is a path you're familiar with.
But if they do apply to you, then this is the blog series for you!
In this first post, we're going to set up a project for using the new REST Builder. I'll introduce part of the Yaml file that defines the entry point. However, since this post would otherwise be quite long, the subsequent parts will continue the work we start here to define the service paths (entry points) and continue the service building.
Pre-Work
Okay, so before we really dive in, take a moment to click on THIS LINK to check out the Liferay documentation on using REST Builder. (https://portal.liferay.dev/docs/7-2/appdev/-/knowledge_base/a/generating-apis-with-rest-builder)
Go on, click the link. It won't take you that long to read it, I can wait...
Back already? I told you that it wouldn't take long...
Starting The Project
Okay, so I'm going to start from scratch here...
First I need a project, so hello blade:
blade init -v 7.2 vitamins
Vitamins? Did I mention that my project is going to be based around vitamins and minerals? Well, maybe this is the first reference. My project is going to be based around vitamins and minerals, and I need to store vitamins and minerals data that are more than just web content. So I need a custom service layer (Service Builder) along with the headless REST layer (REST Builder).
Next I can load the project into my IDE and edit the build.gradle file in the (soon to be discussed) headless-vitamins-impl module per the instructions here: https://portal.liferay.dev/docs/7-2/reference/-/knowledge_base/r/rest-builder-gradle-plugin
One thing that wasn't clear in the referenced documentation: if
you are using the Liferay Gradle Workspace, the build script they
give you goes into the build.gradle file in your
headless-<name>-impl module,
not your settings.gradle file or root level
build.gradle file. If you try to put it into
settings.gradle, you get a weird error about
org.gradle.initialization.DefaultSettings_Decorated cannot be
cast to org.gradle.api.Project
. If you get this, it means
your REST Builder plugin is listed in the wrong file.
A quick "./gradlew tasks" command in the headless-vitamins-impl module (we're creating it below) will show that I do in fact have the REST Builder available:
$ ./gradlew tasks > Task :tasks ------------------------------------------------------------ All tasks runnable from root project ------------------------------------------------------------ Build tasks ----------- assemble - Assembles the outputs of this project. build - Assembles and tests this project. buildCSS - Build CSS files. buildDependents - Assembles and tests this project and all projects that depend on it. buildLang - Runs Liferay Lang Builder to translate language property files. buildNeeded - Assembles and tests this project and all projects it depends on. buildREST - Runs Liferay REST Builder. ...snip...
I need a handful of modules, so I then create the headless-vitamins-api, headless-vitamins-impl, headless-vitamins-client, and headless-vitamins-test modules in the workspace's modules/headless-vitamins folder (I created the subdirectory to keep the headless modules together). The documentation doesn't tell you to create these extra modules, but you're going to want to as they are referenced later.
$ cd modules/headless-vitamins $ blade create -t api -v 7.2 -p com.dnebinger.headless.vitamins headless-vitamins-api Successfully created project headless-vitamins-api in vitamins/modules/headless-vitamins $ blade create -t api -v 7.2 -p com.dnebinger.headless.vitamins headless-vitamins-impl Successfully created project headless-vitamins-impl in vitamins/modules/headless-vitamins $ blade create -t api -v 7.2 -p com.dnebinger.headless.vitamins headless-vitamins-client Successfully created project headless-vitamins-client in vitamins/modules/headless-vitamins $ blade create -t api -v 7.2 -p com.dnebinger.headless.vitamins headless-vitamins-test Successfully created project headless-vitamins-test in vitamins/modules/headless-vitamins
Since I used the api type to create my modules, I've got a bunch of junk packages and java files in the projects; take some time to clean those out.
Also, in the headless-vitamins-test directory, we need to rename the src/main folder to be src/testIntegration. This project is where REST Builder will generate some integration test cases for us, but we have to have the right directory for it to work.
We'll follow Liferay's standard naming patterns for the bundles, so our bnd.bnd files will be updated with com.dnebinger.headless.vitamins.api and com.dnebinger.headless.vitamins.impl symbolic names, etc.
The build.gradle files will take a lot of additions, but we'll push that for a bit.
Defining The Services
So here's where the fun begins... We need to create our Yaml files that define the service endpoints.
If you've never done this before, this is really going to seem daunting. And it fact it can be challenging figuring this stuff out the first time.
First we'll tackle the simple file; in headless-vitamins-impl we need to add the rest-config.yaml file:
apiDir: "../headless-vitamins-api/src/main/java" apiPackagePath: "com.dnebinger.headless.vitamins" application: baseURI: "/headless-vitamins" className: "HeadlessVitaminsApplication" name: "dnebinger.Headless.Vitamins" author: "Dave Nebinger" clientDir: "../headless-vitamins-client/src/main/java" testDir: "../headless-vitamins-test/src/testIntegration/java"
This is pretty much "canned" configuration that you'll see in Liferay's headless plays as well as any that you create. The last two entries? That's where the client and test modules come in, so that's why we added them earlier.
Next is the real fun one, the rest-openapi.yaml file, this one is also created in your headless-vitamins-impl module.
Instead of dumping the whole thing at once, I'm going to piecemeal this thing together, out of order here, to show off the details. You'll be able to find the full file in the repository.
Every OpenAPI Yaml file has 3 sections: Meta, Paths (endpoints) and Reusable Components (type definitions), and mine is no different.
Here's my meta section:
openapi: 3.0.1 info: title: "Headless Vitamins" version: v1.0 description: "API for accessing Vitamin details."
Okay, so far so good.
Defining The Types
Next I'm going to share my Reusable Components. Sure it is out of order, but it will make covering the Paths later on a little easier.
Here's my main type, the Vitamin type:
components: schemas: Vitamin: description: Contains all of the data for a single vitamin or mineral. properties: name: description: The vitamin or mineral name. type: string id: description: The vitamin or mineral internal ID. type: string chemicalNames: description: The chemical names of the vitamin or mineral if it has some. items: type: string type: array properties: description: The chemical properties of the vitamin or mineral if it has some. items: type: string type: array group: description: The group the vitamin or mineral belongs to, i.e. the B group or A group. type: string description: description: The description of the vitamin or mineral. type: string articleId: description: A journal articleId if there is a web content article for this vitamin. type: string type: description: The type of the vitamin or mineral. enum: [Vitamin, Mineral, Other] type: string attributes: description: Health properties attributed to the vitamin or mineral. items: type: string type: array risks: description: Risks associated with the vitamin or mineral. items: type: string type: array symptoms: description: Symptoms associated with the vitamin or mineral deficiency. items: type: string type: array creator: $ref: "#/components/schemas/Creator" type: object
If you haven't seen a Yaml file before, well this is it. Indents signify depth, so the next line at a higher indent is a child, but a line at a same depth is a sibling.
My Vitamin type has a number of properties. Some are simple, like the name and the id, others are a little more complex. The type property is a String, but it is bounded by the enumeration of possible values. The creator is a reference to another object in the file (that's the $ref guy).
When you do have a $ref in the same file, it means you need to include the reference. Here's my Creator type that I copied from Liferay's headless-delivery file:
Creator: description: Represents the user account of the content's creator/author. Properties follow the [creator](https://schema.org/creator) specification. properties: additionalName: description: The author's additional name (e.g., middle name). readOnly: true type: string familyName: description: The author's surname. readOnly: true type: string givenName: description: The author's first name. readOnly: true type: string id: description: The author's ID. format: int64 readOnly: true type: integer image: description: A relative URL to the author's profile image. format: uri readOnly: true type: string name: description: The author's full name. readOnly: true type: string profileURL: description: A relative URL to the author's user profile. format: uri readOnly: true type: string type: object
So that's it for the types.
Conclusion
Concluding so soon? But we're not done!
No worries, the next part is done and ready, so you can head over there next.
In this part, we created a new Liferay Gradle Workspace for our custom headless service and defined the necessary modules we're going to be creating code in.
We also started the OpenAPI Yaml file, starting with the Meta section and skipping over the Path section to cover the Reusable Components section, where we defined the Vitamin and the Creator objects.
In the next part, we'll flush out the paths (entry points).
See you there!