Docs

Endpoints for accessing the Java backend

A server-side Java endpoint is a back-end method that’s exposed for calling from client-side TypeScript code.

An endpoint in Hilla is a class that defines one or more public methods, and is annotated with the @Endpoint annotation.

Hilla bridges Java back-end endpoints and a TypeScript frontend. It generates TypeScript clients to call the Java backend in a type-checkable way. The Endpoint generator reference page contains details about the generator itself.

Important
Hilla endpoints depend on Spring Boot auto-configuration
They don’t work if auto-configuration is disabled, for example when you use @EnableWebMvc. As a workaround, remove the @EnableWebMvc annotation, as described in Spring Boot documentation. If you have an idea for how to make it more useful for you, please share it on GitHub.

Creating an endpoint

An endpoint is a Java class annotated with @Endpoint:

/**
 * An endpoint that counts numbers.
 */
@Endpoint
@AnonymousAllowed
public class CounterEndpoint {
    /**
     * A method that adds one to the argument.
     */
    public int addOne(int number) {
        return number + 1;
    }
}

When the application starts, Hilla scans the classpath for @Endpoint-annotated classes. For each request to access a public method in a Hilla endpoint, a permission check is carried out. @AnonymousAllowed means that Hilla permits anyone to call the method from the client side.

Refer to the Security article for details of configuring endpoint access.

Modules generated from Hilla endpoints

Hilla generates a TypeScript module for every Hilla endpoint on the backend. Each such module exports all the methods in the endpoint.

You can import an entire module from the barrel file, import all methods as a module from the endpoint file, or select individual endpoint methods. For example, the CounterEndpoint.ts could be used, as in the following snippets:

import { CounterEndpoint } from 'Frontend/generated/endpoints';

CounterEndpoint.addOne(1).then((result) => console.log(result));
Note
The barrel file exports all the endpoints at once, so you can import multiple endpoints using a single import.
import * as CounterEndpoint from 'Frontend/generated/CounterEndpoint';

CounterEndpoint.addOne(1).then((result) => console.log(result));
import { addOne } from 'Frontend/generated/CounterEndpoint';

addOne(1).then((result) => console.log(result));
Note
The Frontend directory alias

The 'Frontend/' path prefix is an alias for the {project.basedir}/frontend directory in your application project.

Hilla has this path alias in the default TypeScript compiler configuration (tsconfig.json); the webpack configuration file (webpack.generated.js) respects the tsconfig aliases by default.

Using this path alias is recommended, as it allows for absolute import paths, rather than traversing the directory hierarchy in relative imports.

Hilla generates the TypeScript modules automatically when you compile the application, as well as when the application is running in development mode.

By default, the generated files are located under {project.basedir}/frontend/generated. You can change the folder by providing the path for the generator in the generatedFrontendDirectory property for the Hilla Maven plugin.

Hilla takes care of type conversion between Java and TypeScript types. For more information about supported types, see Type conversion.

Example TypeScript module contents

For example, the generated TypeScript module for the Java endpoint defined in CounterEndpoint.java would look as follows:

/**
 * An endpoint that counts numbers.
 *
 * This module is generated from CounterEndpoint.java
 * All changes to this file are overridden. Please consider to make changes in the corresponding Java file if necessary.
 * @see {@link file:///srv/jenkins/workspace/hilla/hilla-docs/docs/src/main/java/com/vaadin/demo/fusion/accessingbackend/CounterEndpoint.java}
 * @module CounterEndpoint
 */

// @ts-ignore
import client from './connect-client.default';
// @ts-ignore
import { EndpointRequestInit, Subscription } from '@hilla/frontend';


/**
 * A method that adds one to the argument.
 *
 * @param _number
 * @param __init an optional object containing additional parameters for the request
 *
 */
function _addOne(
 _number: number,
 __init?: EndpointRequestInit
): Promise<number>
{
 return client.call('CounterEndpoint', 'addOne', {_number}, __init);
}
export {
  _addOne as addOne,
};

Entities

An endpoint method can return or receive a parameter as an entity (non-primitive type). In this case, the generator also creates a TypeScript interface for the entity.

An entity can be defined in the following ways:

  • In a separate class that belongs to the project.

  • In a class that belongs to the project dependency.

  • In an inner class (of an endpoint or any other class).

package com.vaadin.demo.fusion.accessingbackend;

/**
 * An entity that contains an information about a city.
 */
public class City {
    private final String country;
    private final String name;

    public City(String name, String country) {
        this.country = country;
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public String getCountry() {
        return country;
    }
}
package com.vaadin.demo.fusion.accessingbackend;

import java.util.Arrays;
import java.util.List;

import com.vaadin.flow.server.auth.AnonymousAllowed;
import dev.hilla.Endpoint;

/**
 * A Vaadin endpoint that shows principles of work with entities.
 */
@Endpoint
@AnonymousAllowed
public class CountryEndpoint {
    private final List<City> cities = Arrays.asList(
            new City("Turku", "Finland"), new City("Berlin", "Germany"),
            new City("London", "UK"), new City("New York", "USA"));

    /**
     * A method that returns a collection of entities.
     */
    public List<City> getCities(Query query) {
        return query.getNumberOfCities() <= cities.size() ?
                cities.subList(0, query.getNumberOfCities() - 1) : cities;
    }

    /**
     * An entity specified as an inner class.
     */
    public static class Query {
        private final int numberOfCities;

        public Query(final int numberOfCities) {
            this.numberOfCities = numberOfCities;
        }

        public int getNumberOfCities() {
            return numberOfCities;
        }
    }
}

The TypeScript output is the following:

/**
 * An entity that contains an information about a city.
 * This module is generated from com.vaadin.demo.fusion.accessingbackend.City.
 * All changes to this file are overridden. Please consider to make changes in the corresponding Java file if necessary.
 * @see {@link file:///srv/jenkins/workspace/hilla/hilla-docs/docs/src/main/java/com/vaadin/demo/fusion/accessingbackend/City.java}
 */
export default interface City {
  country?: string;
  name?: string;
}
/**
 * An entity specified as an inner class.
 * This module is generated from com.vaadin.demo.fusion.accessingbackend.CountryEndpoint.Query.
 * All changes to this file are overridden. Please consider to make changes in the corresponding Java file if necessary.
 * @see {@link file:///srv/jenkins/workspace/hilla/hilla-docs/docs/src/main/java/com/vaadin/demo/fusion/accessingbackend/CountryEndpoint.java}
 */
export default interface Query {
  numberOfCities: number;
}
/**
 * A Vaadin endpoint that shows principles of work with entities.
 *
 * This module is generated from CountryEndpoint.java
 * All changes to this file are overridden. Please consider to make changes in the corresponding Java file if necessary.
 * @see {@link file:///srv/jenkins/workspace/hilla/hilla-docs/docs/src/main/java/com/vaadin/demo/fusion/accessingbackend/CountryEndpoint.java}
 * @module CountryEndpoint
 */

// @ts-ignore
import client from './connect-client.default';
// @ts-ignore
import { EndpointRequestInit, Subscription } from '@hilla/frontend';

import type City from './com/vaadin/demo/fusion/accessingbackend/City';
import type Query from './com/vaadin/demo/fusion/accessingbackend/CountryEndpoint/Query';

/**
 * A method that returns a collection of entities.
 *
 * @param query
 * @param __init an optional object containing additional parameters for the request
 *
 */
function _getCities(
 query: Query | undefined,
 __init?: EndpointRequestInit
): Promise<Array<City | undefined> | undefined>
{
 return client.call('CountryEndpoint', 'getCities', {query}, __init);
}
export {
  _getCities as getCities,
};

Nullable and non-nullable types

See Type nullability for more information about how the nullability algorithm works and how to make types non-nullable.

Endpoint URLs

Hilla automatically generates endpoint URLs and wraps them in the generated TypeScript API, so the developer doesn’t need to worry about them.

Any public method in any Hilla endpoint can be accessed with the following URL:

http://${base_url}/${prefix}/${endpoint_name}/${method_name}

where:

${base_url}

The base URL of the application, depending on the framework used. For instance, for the Spring framework, the default URL if the application is started locally is http://localhost:8080. If the application is started with a context, it should be added to the end; for instance, http://localhost:8080/my-app.

${prefix}

The URL common part that every exposed endpoint has. By default, connect is used, but this can be configured in the application properties.

${endpoint_name}

By default, this is the corresponding Java class name which exposes methods, although this can be changed in the @Endpoint annotation value.

${method_name}

The public method name from the Java class.

For an application started locally with the CounterEndpoint endpoint defined as shown, the endpoint URL will be: http://localhost:8080/connect/counterendpoint/addone

@Endpoint
public class CounterEndpoint {

    public int addOne(int number) {
        return number + 1;
    }
}
Note
Endpoint URLs aren’t case-sensitive
The endpoint name and the method name aren’t case-sensitive in Hilla, so the URL shown is the same as http://localhost:8080/connect/CounterEndpoint/addOne or http://localhost:8080/connect/COUNTERENDPOINT/ADDONE, or any other case combination for the endpoint and method name.

Configuring endpoint URLs

You can configure the following parts of the URL:

${prefix}

The default value is connect. To change it to some other value, provide an application.properties file in the project resources (src/main/resources/application.properties) and set the vaadin.endpoint.prefix property to the new value.

${endpoint_name}

By default, the simple name of the Java class is taken. It’s possible to specify a value in the @Endpoint annotation to override default one (@Endpoint("customName")). In this case, the customName value is used as a ${endpoint_name} to accept incoming requests; it’s also case-insensitive.

Endpoint method validation

The parameters of an endpoint method are automatically validated and, if validation fails, a corresponding response is sent back to the browser.

Whenever an endpoint method is invoked, its parameters are automatically validated using the JSR 380 Bean validation specification after they are deserialized from the endpoint request body.

This is useful in eliminating the boilerplate needed for the initial request validation. The framework automatically checks the constraints placed on beans and sends the response back to the client side if the validation fails. The browser raises an EndpointValidationError when it receives the corresponding response from the server.

Built-in validation constraints

The built-in validation constraints are the set of annotations provided by the javax.validation.validation-api dependency. They are intended to be placed on Java beans on the server side.

You can find a full list of the constraints at https://beanvalidation.org/2.0/spec#builtinconstraints

To use these annotations, add them to the class field or method parameter. For example:

public class Account {

  @Positive
  private Long id;

  @NotEmpty(message = "Each account must have a non-empty username")
  private String username;

  private void sendAccountData(@NotNull String destination) {
    // ...
  }
}

Custom validation constraints

It’s possible to create custom constraints. To do this, you need to create a custom annotation and a custom validator.

See the official documentation for more details.

Manual validation

Since all the dependencies needed for validating beans and methods are present, you can reuse them in any part of your project, not only in the endpoint methods. For example:

// A validator for validating beans
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
// non-empty set if there are any constraint validation errors
Set<ConstraintViolation<Object>> violations = validator.validate(bean);

// A validator for validating methods and constructors (return values, parameters)
ExecutableValidator executableValidator = validator.forExecutables();
// non-empty set if there are any constraint validation errors
Set<ConstraintViolation<Object>> violations = executableValidator.validateReturnValue(object, method, returnValue);

If required, you can throw an EndpointValidationException from an endpoint method. This exception will be caught by TypeScript and the corresponding EndpointValidationError will be raised.

See the official documentation for more details on validating bean constraints and validating method constraints.

Hilla validation implementation details

Hilla validates only the beans and method parameters that are used in the endpoint classes (i.e. classes with the @Endpoint annotation). No other types are validated, even if they have constraint annotations on them.

If any validation errors occur, a non-200 response is sent back, which is interpreted in TypeScript as a reason to throw an EndpointValidationError. A similar effect is achieved if an EndpointValidationException is thrown by any of the Java endpoint methods.

Error handling

A robust client implementation should be able to deal with invalid endpoint calls, errors on the server side, and network outages.

Hilla determines the success of an endpoint call by inspecting the HTTP status code. The server returns the 200 OK code when it’s able to process the request successfully, deserialize the method body, find and execute the particular method in the endpoint, and serialize its return value into a response.

If the status code of the response isn’t 200 OK, Hilla throws an error on the client side. The available parameters in the error and the specific class of the thrown error depend on the failure mode. The most common ones are described below.

Missing endpoint

If the request addresses an endpoint or a method name not present on the backend, the server responds with 404 Not Found and Hilla raises an error of type EndpointError.

Parameter validation error

If the method called in the request exists on the backend, but the parameter count and types don’t match the endpoint method (see Type conversion between JavaScript and Java for more details about the type conversion rules), the server responds with 400 Bad Request and Hilla raises an error of type EndpointValidationException. The error instance contains a field validationErrorData holding validation error information for each invalid parameter.

For example, given the following endpoint expecting a java.time.LocalDate parameter:

package com.vaadin.demo.fusion.errorhandling;

import java.time.LocalDate;

import dev.hilla.Endpoint;
import com.vaadin.flow.server.auth.AnonymousAllowed;

@Endpoint
public class DateEndpoint {

    @AnonymousAllowed
    public LocalDate getTomorrow(LocalDate date) {
        return date.plusDays(1);
    }
}

A call with an illegal data parameter raises an EndpointValidationException with information about which parameters failed validation:

import { EndpointValidationError } from '@hilla/frontend';

import { DateEndpoint } from 'Frontend/generated/endpoints';

export async function callEndpoint() {
  try {
    // pass an illegal date
    const tomorrow = await DateEndpoint.getTomorrow('2021-02-29');
    console.log(tomorrow);
    // handle result...
  } catch (error) {
    if (error instanceof EndpointValidationError) {
      (error as EndpointValidationError).validationErrorData.forEach(
        ({ parameterName, message }) => {
          console.warn(parameterName); // "date"
          console.warn(message); // "Unable to deserialize an endpoint method parameter into type 'java.time.LocalDate'"
        }
      );
    } else {
      // handle other error types...
    }
  }
}

Server-side errors

If the endpoint exists and its parameters could be passed, but its execution raises a Java runtime exception, the server responds with 500 Internal Server Error and Hilla raises an error of type EndpointError. As a special case, if the server-side exception is an instance of dev.hilla.exception.EndpointException or a subclass, the server instead responds with 400 Bad Request and the exception type and message passed to the EndpointException in Java are available in the EndpointError instance via the type and message attributes. For example, given the following endpoint implementation:

package com.vaadin.demo.pwa.offline;

import dev.hilla.Endpoint;
import dev.hilla.exception.EndpointException;

@Endpoint
public class DataEndpoint {

    public String getViewData() {
        throw new EndpointException("Not implemented");
    }
}

The following client-side call to the endpoint method logs the error message and exception type:

import { EndpointError } from '@hilla/frontend';

import { DataEndpoint } from 'Frontend/generated/endpoints';

export async function callEndpoint() {
  try {
    await DataEndpoint.getViewData();
  } catch (error) {
    if (error instanceof EndpointError) {
      console.warn((error as EndpointError).message); // "Not implemented"
      console.warn((error as EndpointError).type); // "dev.hilla.exception.EndpointException"
    }
  }
}

Network errors

When the server isn’t reachable due to outage or network disruption, an endpoint call results in a low-level network error, different from EndpointError. Applications that support offline mode can wrap endpoint calls with exception-handling code returning a fallback value, by distinguishing between the error classes as follows:

import { EndpointError } from '@hilla/frontend';

// import the remote endpoint
import { DataEndpoint } from 'Frontend/generated/endpoints';

// wrap endpoint calls to return fallback data when offline
export async function getViewData() {
  try {
    return await DataEndpoint.getViewData();
  } catch (e) {
    if (!(e instanceof EndpointError)) {
      // network failure: return fallback data
      return [];
    } else {
      // endpoint reached but returned abnormal status code:
      // pass exception on to caller
      throw e;
    }
  }
}

Also, see documentation about caching endpoint data in local storage using a generic wrapper.

Unexpected response contents

If the server replies with a response other than 200 OK, and the string contained in the response isn’t valid JSON, an EndpointResponseError is raised. The exception contains the response text as message and the Response object in the response field.

Code completion in IDEs

As you can see in the earlier CounterEndpoint.ts example, the Javadoc for the @Endpoint class is copied to the generated TypeScript file, and the type definitions are maintained. This helps code completion to work, at least in Visual Studio Code and IntelliJ IDEA Ultimate Edition.

Code-completion
Code completion in Visual Studio Code