Creating Headless APIs (Part 1)

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

Take a moment to enable the target platform in the gradle.properties; you'll be glad you did. I won't be including versions in my build.gradle files since I'm using the target platform.

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.

Editing Yaml files with a simple text editor is hard. It can be daunting getting the indenting correct, especially when it comes to line wrapping, etc. If you don't have a suitable editor, I recommend using Swagger's online tool at  https://editor.swagger.io/ Not only will it handle the indenting correctly, but it also has code completion facilities that will really help you out if you're new to the OpenAPI Yaml format.

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!

https://github.com/dnebing/vitamins

Blogs

David,

Thanks for an excellent post. Does Liferay have a documentation where we can look at all the possible options that go into the file: `config.yaml` and any other yaml files? 

 

Please let us know. 

Thanks in advance!

Not right now, but I know the team is under a big push to complete the documentation effort so it could be coming soon.

 

Me, I just used the Liferay examples to find out what the tooling would allow and or require me to use. I was just doing a pretty basic implementation, so I certainly didn't need anything the Liferay examples weren't already doing.