Docs

Browser-Callable Services

Java services whose public methods React views call through generated TypeScript modules.

A browser-callable service is a Java class whose public methods React views can call. Vaadin generates a TypeScript module for each service, with a function for each method, so that views call the methods in a type-safe way. For a step-by-step guide, see Call Java Services from React Views.

Browser-callable services need the Hilla Spring Boot starter, as described in Enabling React Views. They also rely on the Spring MVC auto-configuration of Spring Boot. If an application turns that auto-configuration off with @EnableWebMvc, the requests to its services don’t reach them.

Reactive Services
Service methods that return a stream of values, and the subscriptions that React views get for them.
TypeScript Client
The ConnectClient that sends the calls of React views to browser-callable services, and the middleware that intercepts them.
Type Conversion
How the Java types of browser-callable services map to TypeScript types, and how their values travel as JSON.
Type Nullability
How the Java types and their nullability annotations decide which generated TypeScript types allow undefined.
Code Generation
When Vaadin generates the TypeScript code of browser-callable services, which files it writes, and how it finds the services.

Creating a Service

Annotate the service class with @BrowserCallable. The annotation also makes the class a Spring bean, so the class can get its dependencies through its constructor:

Source code
CounterService.java

Access to services is denied by default. Give the class or each method an access annotation, such as @AnonymousAllowed, @PermitAll, or @RolesAllowed. See Service Access Control.

The @Endpoint annotation is an alias for @BrowserCallable. Its optional value sets the name of the service, which is otherwise the simple name of the class. The generated module and the requests use that name.

Inherited Methods

Vaadin generates TypeScript functions for the methods declared in the service class, and for the methods of its superclasses and interfaces that are annotated with @EndpointExposed:

Source code
Java
@EndpointExposed
public class ExposedClass {
    public String fromExposedClass() { 1
        return "Hello from ExposedClass";
    }
}

public class NonExposedClass extends ExposedClass {
    public String fromNonExposedClass() { 2
        return "Hello from NonExposedClass";
    }
}

@BrowserCallable
@AnonymousAllowed
public class ServiceClass extends NonExposedClass {
    public String fromServiceClass() { 3
        return "Hello from ServiceClass";
    }
}
  1. Gets a function, because its class is annotated.

  2. Gets no function, because its class isn’t annotated.

  3. Gets a function, because it’s declared in the service class.

The annotation only controls which functions Vaadin generates. A request can call every public method of the service, including a method without a function, such as fromNonExposedClass() in the example. The access annotations of the service class apply to the inherited methods. Don’t inherit public methods that the browser mustn’t call.

Generated Modules

For each service, Vaadin generates a TypeScript module in src/main/frontend/generated, named after the service. The module exports a function for each method. Each function takes the parameters of the method, followed by an optional argument with request options, and returns a Promise of the return value. When Vaadin generates the modules is described in Code Generation.

The generated module for the service in Creating a Service is as follows:

Source code
CounterService.ts
import type { EndpointRequestInit } from '@vaadin/hilla-frontend';
import client from './connect-client.default.js';

async function addOne(number: number, init?: EndpointRequestInit): Promise<number> {
  return client.call('CounterService', 'addOne', { number }, init);
}

export { addOne };
CounterService.ts

Import the service from the endpoints.ts module, which exports all the services:

Source code
barrel.ts
import { CounterService } from 'Frontend/generated/endpoints';

CounterService.addOne(1).then((result) => console.log(result));
barrel.ts

You can also import a module of one service as an object, or import single functions from it:

Source code
asterisk.ts
import * as CounterService from 'Frontend/generated/CounterService';

CounterService.addOne(1).then((result) => console.log(result));
asterisk.ts
Source code
method.ts
import { addOne } from 'Frontend/generated/CounterService';

addOne(1).then((result) => console.log(result));
method.ts

The Frontend/ prefix of the imports is an alias for the src/main/frontend directory.

For the Java types used in the parameters and return values of the methods, Vaadin generates TypeScript interfaces. For example, this service returns a list of City objects:

Source code
City.java
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;
    }
}
City.java
Source code
CountryService.java
package com.vaadin.demo.fusion.accessingbackend;

import com.vaadin.flow.server.auth.AnonymousAllowed;
import com.vaadin.hilla.BrowserCallable;

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

/**
 * A browser-callable service that shows principles of work with entities.
 */
@BrowserCallable
@AnonymousAllowed
public class CountryService {
    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())
                : 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;
        }
    }
}
CountryService.java

Vaadin generates this interface for City:

Source code
City.ts
interface City {
  name?: string;
  country?: string;
}

export default City;
City.ts

How Java types map to TypeScript types, and which of them can be undefined, is described in Type Conversion and Type Nullability.

Request Options

The last argument of a generated function is an optional object with these options:

signal

An AbortSignal that cancels the request. A cancelled call rejects with a DOMException named AbortError. One AbortController can cancel several calls when they get the same signal.

mute

If true, the call doesn’t show the loading indicator.

For example:

Source code
request-options.ts
Source code
request-options.ts

The functions of reactive services have no request options.

Service URLs

A generated function sends a POST request to connect/{service}/{method}, relative to the base URL of the application. The body is a JSON object with the arguments by parameter name. The service and method names in the URL aren’t case-sensitive. Call the services through the generated functions rather than through these URLs.

To serve the services under another prefix than connect, set the vaadin.endpoint.prefix property, for example in application.properties:

Source code
properties
vaadin.endpoint.prefix=/api

The generated client still uses connect. Give the browser the same prefix with a client of your own, without the leading slash:

Source code
src/main/frontend/connect-client.ts
import { ConnectClient } from '@vaadin/hilla-frontend';

const client = new ConnectClient({ prefix: 'api' });

export default client;

Without it, the calls get the HTML page of the application instead of a JSON response, and fail with a SyntaxError.

Parameter Validation

Before calling a method, Vaadin checks its arguments with Jakarta Bean Validation. It checks the constraints on the parameters of the method, and the constraints on the fields of objects passed as arguments:

Source code
Java
public class Account {

    @Positive
    private Long id;

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

    // Getters and setters
}

@BrowserCallable
@AnonymousAllowed
public class AccountService {

    public void save(Account account, @NotNull String destination) {
        // ...
    }
}

When an argument can’t be converted to its Java type, or a constraint fails, the call fails with an EndpointValidationError. Vaadin checks the constraints on the parameters only after the arguments are converted and the objects pass their own constraints. The validationErrorData property of the error lists the problems found, with these properties for each:

parameterName

Where the problem is. For a constraint on a parameter, it’s the method name and the parameter name, such as save.destination. For a constraint on a field of an object, it’s the path of the field in the object, such as username.

message

A description of the problem.

validatorMessage

The message of the failed constraint, if a constraint failed.

The following example catches a value that can’t be converted to a LocalDate:

Source code
DateService.java
package com.vaadin.demo.fusion.errorhandling;

import com.vaadin.flow.server.auth.AnonymousAllowed;
import com.vaadin.hilla.BrowserCallable;

import java.time.LocalDate;

@BrowserCallable
public class DateService {

    @AnonymousAllowed
    public LocalDate getTomorrow(LocalDate date) {
        return date.plusDays(1);
    }
}
DateService.java
Source code
catch-invalid-args.ts
import { EndpointValidationError } from '@vaadin/hilla-frontend';
import { DateService } from 'Frontend/generated/endpoints';

export async function callService() {
  try {
    // pass an illegal date
    const tomorrow = await DateService.getTomorrow('2021-02-29');
    console.log(tomorrow);
    // handle result...
  } catch (error) {
    if (error instanceof EndpointValidationError) {
      error.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...
    }
  }
}
catch-invalid-args.ts

A method can also fail a call with an EndpointValidationError by throwing an EndpointValidationException. Vaadin also checks the constraints on the return value. When the return value fails them, the call fails as described in Errors.

When a form bound with the useForm hook submits its value to a service method, the form shows the validation errors of the call in its fields. See Server-Side Validation.

Errors

A call that fails rejects with one of the errors below. The error classes come from @vaadin/hilla-frontend. EndpointValidationError and EndpointResponseError extend EndpointError, and UnauthorizedResponseError and ForbiddenResponseError extend EndpointResponseError.

Cause Status Error

The method throws an EndpointException.

400

EndpointError

An argument can’t be converted or fails validation.

400

EndpointValidationError

The call has the wrong number of arguments.

400

EndpointResponseError

Access is denied to a user who isn’t logged in, or the request has no valid CSRF token.

401

UnauthorizedResponseError

Access is denied to a logged-in user.

403

ForbiddenResponseError

The service or the method doesn’t exist.

404

EndpointResponseError

The method throws another exception, or its return value is an unexpected null or fails validation.

500

EndpointResponseError

The request is cancelled.

—

DOMException named AbortError

The server can’t be reached.

—

TypeError, from the Fetch API

An EndpointResponseError has the HTTP response in its response property, and the status code in status. When the method throws an exception other than an EndpointException, the message only says that the method failed. The server logs the exception, but doesn’t send its details to the browser. When the return value is the problem, the message describes it.

To send the details of a failure to the browser, throw an EndpointException, or a subclass of it. The EndpointError then has these properties:

message

The message of the exception.

type

The fully qualified class name of the exception, or of its cause when the exception was created with one.

detail

The detail argument of the exception, converted to JSON. Use it for data that the view needs to handle the error.

For example:

Source code
DataService.java
package com.vaadin.demo.pwa.offline;

import com.vaadin.flow.server.auth.AnonymousAllowed;
import com.vaadin.hilla.BrowserCallable;
import com.vaadin.hilla.exception.EndpointException;

@BrowserCallable
@AnonymousAllowed
public class DataService {

    public String getViewData() {
        throw new EndpointException("Not implemented");
    }
}
DataService.java
Source code
catch-error.ts
import { EndpointError } from '@vaadin/hilla-frontend';
import { DataService } from 'Frontend/generated/endpoints';

export async function callService() {
  try {
    await DataService.getViewData();
  } catch (error) {
    if (error instanceof EndpointError) {
      console.warn(error.message); // "Not implemented"
      console.warn(error.type); // "com.vaadin.hilla.exception.EndpointException"
    }
  }
}
catch-error.ts

When the server can’t be reached, the error isn’t an EndpointError. A view that should work offline can return fallback data in that case:

Source code
ts-view-with-service.ts
import { EndpointError } from '@vaadin/hilla-frontend';
// Import the remote service
import { DataService } from 'Frontend/generated/endpoints';

// Wrap service calls to return fallback data when offline
export async function getViewData() {
  try {
    return await DataService.getViewData();
  } catch (e) {
    if (!(e instanceof EndpointError)) {
      // Network failure: return fallback data
      return [];
    }

    // Service reached but returned abnormal status code:
    // pass exception on to caller
    throw e;
  }
}
ts-view-with-service.ts

Updated –