Producing and Implementing APIs with REST Builder
With REST Builder, you can define the API you want to build, and REST Builder provides the framework and endpoints for you.
Deploy an Example REST API
To see REST Builder in action, you can deploy an example API that retrieves a dummy product by its ID in a catalog. Once you understand how this simple example works, you can create APIs for your own applications.
Start a new Liferay instance by running
docker run -it -m 8g -p 8080:8080 liferay/portal:7.4.3.132-ga132
Sign in to Liferay at http://localhost:8080. Use the email address test@liferay.com and the password test. When prompted, change the password to learn.
Then, follow these steps:
-
Download and unzip the
.ziparchive containing the Acme Foo API:curl https://resources.learn.liferay.com/examples/liferay-r3b2.zip -Ounzip liferay-r3b2.zip -
Build and deploy the example:
./gradlew deploy -Ddeploy.docker.container.id=$(docker ps -lq)NoteThis command is the same as copying the deployed jars to /opt/liferay/osgi/modules on the Docker container.
-
Confirm the deployment in the Docker container console for both the
apiandimplbundles:STARTED com.acme.headless.r3b2.api_1.0.0 STARTED com.acme.headless.r3b2.impl_1.0.0 -
Log into your DXP instance and navigate to the Global Menu (
) → Control Panel → Gogo Shell. -
In the Gogo Shell prompt, type the following command:
jaxrs:checkThe page lists all of the installed JAX-RS bundles, including the newly deployed API,
Liferay.Headless.R3B2. The API is now deployed and ready for you to call.
-
Test the API by running the following command from your terminal, substituting a number between 1 and 3 for
{fooId}:curl -u 'test@liferay.com:learn' "http://localhost:8080/o/headless-r3b2/v1.0/foo/{fooId}"The query returns a corresponding product’s ID, name, and description wrapped in a JSON object:
{ "description": "Universal truth must be transcendental.", "id": 1, "name": "Truth" }
Congratulations, you’ve successfully deployed and used a new REST API.
Now that you’ve seen an API generated with REST Builder, it’s time to understand how it works.
Initial Setup
Begin by creating impl and api modules in your Liferay workspace project. Your impl module’s build.gradle file must install and apply REST Builder as a plugin:
buildscript {
dependencies {
classpath group: "com.liferay", name: "com.liferay.gradle.plugins.rest.builder", version: "1.1.32"
}
repositories {
maven {
url "https://repository-cdn.liferay.com/nexus/content/groups/public"
}
}
}
apply plugin: "com.liferay.portal.tools.rest.builder"
dependencies {
compileOnly group: "com.liferay.portal", name: "release.portal.api"
compileOnly project(":headless-r3b2-api")
}
Your build.gradle files in both modules must also declare dependencies on the portal release.
YAML configuration
Your first step is to create the REST Builder configuration files. In the impl module’s root folder, add two files: rest-config.yaml and rest-openapi.yaml. These files must contain all the information necessary for REST Builder to generate the scaffolding code for your API.
Add REST Builder Configuration
REST Builder configuration belongs in the rest-config.yaml file. It defines the following fields:
apiDir: your Java source code folder
apiPackagePath: the starting Java package path where REST Builder generates code across all modules
baseURI: the context URL for all APIs in this project
className: the Java class name for the root resource class (used by JAX-RS)
javaEEPackage: the namespace to use for code generation. This namespace must match the Liferay version on which you’re deploying your APIs. By default, REST Builder generates code using the Java EE namespace (javax). If you use a recent version of Liferay that’s moved to Jakarta, set the value to jakarta to use the Jakarta EE namespace.
name: the JAX-RS name of the API
Define these fields using this structure:
apiDir: "../headless-r3b2-api/src/main/java"
apiPackagePath: "com.acme.headless.r3b2"
application:
baseURI: "/headless-r3b2"
className: "HeadlessR3B2Application"
name: "Liferay.Headless.R3B2"
author: "Jonah the son of Amittai"
clientDir: "../headless-r3b2-client/src/main/java"
testDir: "../headless-r3b2-test/src/testIntegration/java"
Add an Information Block to the OpenAPI Configuration
Next, open the rest-openapi.yaml file to begin configuring your APIs.
The first section to add is the information block:
info:
description:
"API to return a Foo."
license:
name: "Apache 2.0"
url: "http://www.apache.org/licenses/LICENSE-2.0.html"
title: "Headless R3B2"
version: v1.0
openapi: 3.0.1
The version field defined here becomes part of the URL when your API paths are exposed within your Liferay instance.
Define the Necessary Schemas
Next, in the components block, define schemas for your entities. REST Builder uses what you define here to create corresponding Java beans to represent these entities.
Define a schema block for each entity you want to represent:
components:
schemas:
Foo:
properties:
description:
type: string
id:
format: int64
type: integer
name:
type: string
type: object
Goo:
properties:
description:
type: string
fooId:
format: int64
type: integer
id:
format: int64
type: integer
name:
type: string
In this example, a schema called Foo represents the important data for the use of this API. The Goo entity is linked to Foo by use of the fooId. See the OpenAPI specification for a list of supported data types for schemas.
Your schema definition determines the names of the classes REST Builder generates, including the scaffolding and templates in the resources files. Because the above schemas are called Foo and Bar, the implementation logic belongs in the FooResourceImpl and GooResourceImpl classes.
Define Your APIs
Finally, add the paths block. This must include all APIs that you plan to implement with REST Builder. Here’s a small snippet of the paths block:
paths:
"/foo":
get:
# operations for get and post go here. See the project for full source code.
# ...
"/foo/{fooId}":
get:
operationId: getFoo
# ...
responses:
200:
content:
application/json:
schema:
$ref: "#/components/schemas/Foo"
application/xml:
schema:
$ref: "#/components/schemas/Foo"
# Place other operations, such as get, patch, and put here. See the project for full source code.
"/foo/{fooId}/goos":
get:
operationId: getFooGoosPage
# This is the relationship between Foos and Goos.
# Place your get and post operations here.
# ...
"/goo/{gooId}":
delete:
operationId: deleteGoo
# Place operations on other entities as needed.
You can add paths for different kinds of requests, including get, post, put, patch, and delete.
The path (foo/{fooId}) specifies that this API (getFoo) can be reached by appending the path string to the end of the URL (which also includes the baseURI and version values from your rest-config.yaml file). For instance, this example API is accessed via the full URL: localhost:8080/o/headless-r3b2/v1.0/foo/{fooId}.
The value you substitute for fooId is used as the parameter with the matching name.
Each path has a responses block beneath the parameters block (and within the get block) that defines at least the response for a successful call (indicated by a 200 response).
This responses block specifies that a successful call returns a Product. The string #/components/schemas/Foo references the schema defined earlier in the same file, allowing REST Builder to use the Foo schema as the return type for this API.
Lastly, add a tags definition for this path, beneath the responses block:
tags: ["Foo"]
The tag specifies information that is added to the generated documentation when REST Builder annotates your scaffolding code. The tag name should reflect the name of your schema.
See the rest-openapi.yaml file you downloaded earlier for a complete reference.
There’s also a Goo object to show how you might do relationships: Goos are related to Foos in the sense that they are associated with a fooId.
Run REST Builder
Now that you have added all of the configuration necessary for REST Builder to do most of the work, run the following command from within your impl module to run the buildREST Gradle task:
../gradlew buildREST
REST Builder uses your configuration and populates both your api and impl classes with scaffolding code, as well as the Java classes where you can add your implementation logic.
GraphQL endpoint code and JAX-RS application code are both generated in the graphql and jaxrs packages, respectively. Your own API implementation is added into the appropriate *ResourceImpl class in the resource package.
Add Your Implementation Logic
The last step is to define the logic for each API you have defined. Within your impl module, find the Java resource class where your implementation goes, based on the schema name you defined in rest-openapi.yaml (in this example, FooResourceImpl.java and GooResourceImpl.java).
The location of the class for your implementation depends on the value you defined for apiPackagePath in your rest-config.yaml file. Follow that path and then navigate into internal/resource/<version>/ within it. If you used the same path as this example, then the file is located within src/main/java/com/acme/headless/r3b2/internal/resource/v1_0/.
The implementation class ([SchemaName]ResourceImpl) is located beside the base class (Base[SchemaName]ResourceImpl). Open the implementation class. Since this is just an example, this implementation uses a pre-populated HashTable, and the getFoo method returns the product from the HashTable with the matching fooId. See FooResourceImpl.java in the project for the full implementation.
@Override
public Foo getFoo(Integer fooId) {
return _foos.get(fooId);
}
This method overrides the base method defined in the base class (Base[SchemaName]ResourceImpl), which is defined using special JAX-RS annotations.
You can add any business logic to complete the request. REST Builder only creates a default constructor for the object you defined in your schema. This example business logic creates an object and adds values to it (based on how you defined its parameters in rest-openapi.yaml):
Foo foo1 = new Foo() {
{
description = "Universal truth must be transcendental.";
id = 1L;
name = "Truth";
}
};
The Goo logic is similar, except in this case multiple Goos are returned because Foo objects can contain multiple Goos. When returning a collection of objects, you must use a pagination-friendly object called a Page:
@Override
public Page<Goo> getFooGoosPage(Long fooId) {
List<Goo> goos = new ArrayList<>();
for (Goo goo : _goos.values()) {
if (Objects.equals(fooId, goo.getFooId())) {
goos.add(goo);
}
}
return Page.of(goos);
}
Add a GraphQL Namespace
Liferay supports namespace configuration for your GraphQL applications. You can use namespaces to group queries, mutations, and types for a given application. Specifying namespaces also allows you to use duplicate schemas in different applications.
See Consuming GraphQL APIs for more information on using GraphQL for Liferay applications.
To add and use a GraphQL namespace, follow these steps:
-
Open the
rest-config.yamlfile in your application’simplmodule. -
Add the
graphQLNamespacefield with the desired namespace name.graphQLNamespace: "<namespace name>" -
Add the namespace a level below the desired query or mutation objects.
query{ <namespace name> { ... } } -
Execute REST Builder to publish all designated queries and mutations in the namespace.
You can also access namespaces in your GraphQL extensions with the GraphQLContributor interface. Implement the getGraphQLNamespace method to return the desired namespace name.
public class MyGraphQLContributor implements GraphQLContributor {
...
@Override
public String getGraphQLNamespace() {
return "<namespace name>";
}
...
}
Namespaces are disabled by default in GraphQL extensions.
Conclusion
Congratulations! You now know the basics of implementing a new API with REST Builder and have added a new API to DXP.