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.120-ga120
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
.zip
archive containing the Acme Foo API:curl https://resources.learn.liferay.com/dxp/latest/en/headless-delivery/apis-with-rest-builder/liferay-r3b2.zip -O
unzip 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
api
andimpl
bundles: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:check
The 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)
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 Goo
s are returned because Foo
objects can contain multiple Goo
s. 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.yaml
file in your application’simpl
module. -
Add the
graphQLNamespace
field 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.