Endpoint Generator
Features
The generator has the following noteworthy features:
-
Support for multi-module projects: you can use standard Maven modules in your application, or even external dependencies, as there is no need for endpoints and entity classes to be in the same project as the Hilla application;
-
Designed to be flexible: all parts of the generator are pluggable, which allows you to alter the default behavior or add a new one.
Important
|
Enable the Java compiler "parameters" option
You need to use the javac -parameters option to enable support for multi-module projects and all JVM languages. See Configuration for details.
|
Generator Architecture
The generator consists of three parts:
- Java bytecode parser
-
The parser reads the Java bytecode and generates an OpenAPI scheme.
- TypeScript Abstract Syntax Tree (AST) generator
-
The AST generator reads the OpenAPI scheme and generates TypeScript endpoints that could be used in further frontend development.
- Runtime controller
-
The runtime controller provides runtime communication between the server and the client.
Hilla uses the OpenAPI Specification as a middle layer between endpoints and TypeScript endpoint clients. The implementation is based on OpenAPI specification 3.0. For details, see the appendix at the end of this page.
Examples
Generated TypeScript Endpoint
The UserEndpoint.ts
class is generated from the UserEndpoint.java
class.
/**
* User endpoint.
*
* This module has been generated from UserEndpoint.java
* @module UserEndpoint
*/
import client from './connect-client.default'; // (1)
/**
* Check if a user is admin or not.
*
* @param id User id to be checked
* Return Return true if the given user is an admin, otherwise false.
*/
export async function isAdmin( // (2)
id?: number
) {
return await client.call('UserEndpoint', 'isAdmin', {id});
}
-
This line is a static part of any generated TypeScript class.
connect-client.default.ts
is another generated file, which includes default configurations for theConnectClient
and exports its instance asclient
. -
Each method in the generated TypeScript class corresponds to a Java method in the
@Endpoint
-annotated class.
For more information about type mapping between Java and TypeScript, see Type conversion. You may also want to learn about Type nullability.
Adding a Custom Generator Plugin
Generator plugins can be configured and extended.
This example defines a custom NonNull
annotation and uses it instead of the default one.
The configuration parameters are specific to the plugin.
In this case, the simplest way is to <disable>
the default configuration of the NonnullPlugin
and <use>
a detailed custom configuration, like in this example:
<configuration>
<parser>
<plugins>
<use>
<plugin>
<name>com.vaadin.hilla.parser.plugins.nonnull.NonnullPlugin</name>
<configuration implementation="com.vaadin.hilla.parser.plugins.nonnull.NonnullPluginConfig">
<use>
<annotation>
<name>com.example.application.annotations.NeverNull</name>
<makesNullable>false</makesNullable>
<score>50</score>
</annotation>
</use>
</configuration>
</plugin>
</use>
<disable>
<plugin>
<name>com.vaadin.hilla.parser.plugins.nonnull.NonnullPlugin</name>
</plugin>
</disable>
</plugins>
</parser>
</configuration>
You need to create the custom annotation and update the endpoint to use it:
package com.example.application.annotations;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE_USE })
public @interface NeverNull {
}
@Endpoint
public class MyEndpoint {
@NeverNull
public String sayHello(@NeverNull String name) {
if (name.isEmpty()) {
return "Hello stranger";
} else {
return "Hello " + name;
}
}
}
The plugin configuration is modelled on the configuration classes defined for each plugin. For example, see the Nonnull plugin configuration.
Appendix: How a TypeScript class is generated from the OpenAPI specification
Modules / Classes
The generator collects all the tags
fields of all operations in the OpenAPI document.
Each tag generates a corresponding TypeScript file.
The tag name is used for TypeScript module/class name, as well as the file name.
The TsDoc of the class is fetched from the description
field of the tag object that has the same name as the class.
Methods
Each exported method in a module corresponds to a POST operation of a path item in paths object.
Note
|
The generator only supports the POST operation.
If a path item contains operations other than POST , the generator stops processing.
|
The path must start with /
, as described in Patterned Fields.
It’s parsed as /<endpoint name>/<method name>
, which is used as a parameter to call to Java endpoints in the backend.
The method name from the path is also reused as the method name in the generated TypeScript file.
Method Parameters
The parameters of the method are taken from the application/json
content of the request body object.
To get the result as UserEndpoint.ts
, the request body content should be:
{
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"id": {
"type": "number",
"description": "User id to be checked"
}
}
}
}
}
}
The type and description of each property are used for the TsDoc that describes the parameter in more detail.
Note
|
All the other content types of the request body object are ignored by the Hilla generator.
This means that a method that doesn’t have the |
Method Return Type
The return type and its description are taken from the 200
response object.
As with the request body object, the generator is only interested in the application/json
content type.
The schema type indicates the return type and the description describes the result.
Here is an example of a response object:
{
"200": {
"description": "Return true if the given user is an admin, otherwise false.",
"content": {
"application/json": {
"schema": {
"type": "boolean"
}
}
}
}
}
Note
|
Currently, the generator only recognizes |
Method TsDoc
The TsDoc of the generated method is stored as the description
value of the POST
operation in the path item.
A valid _POST` operation combined with Request body and Response object would look like this:
{
"tags": ["UserEndpoint"], // (1)
"description": "Check if a user is admin or not.",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"id": {
"type": "number",
"description": "User id to be checked"
}
}
}
}
}
},
"responses": {
"200": {
"description": "Return true if the given user is an admin, otherwise false.",
"content": {
"application/json": {
"schema": {
"type": "boolean"
}
}
}
}
}
}
-
As mentioned in the operation object specification, in the Hilla generator,
tags
are used to classify operations into TypeScript files. This means that each tag has a corresponding generated TypeScript file. Operations that contain more than one tag appear in all the generated files. Operations with empty tags are placed in theDefault.ts
file.
Note
| Although multiple tags don’t break the generator, it might be confusing at development time if there are two identical methods in different TypeScript files. It’s recommended to have only one tag per operation. |
Here is an example OpenAPI document that could generate previous UserEndpoint.ts
.
{
"openapi" : "3.0.1",
"info" : {
"title" : "My example application",
"version" : "1.0.0"
},
"servers" : [ {
"url" : "https://myhost.com/myendpoint",
"description" : "Hilla backend server"
} ],
"tags" : [ {
"name" : "UserEndpoint",
"description" : "User endpoint class."
} ],
"paths" : {
"/UserEndpoint/isAdmin" : {
"post": {
"tags": ["UserEndpoint"],
"description": "Check if a user is admin or not.",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [ "id" ],
"properties": {
"id": {
"type": "number",
"description": "User id to be checked"
}
}
}
}
}
},
"responses": {
"200": {
"description": "Return true if the given user is an admin, otherwise false.",
"content": {
"application/json": {
"schema": {
"type": "boolean"
}
}
}
}
}
}
}
}
}