Browser-Callable Services
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";
}
}-
Gets a function, because its class is annotated.
-
Gets no function, because its class isn’t annotated.
-
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
CounterService.tsimport 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
CounterService.tsImport 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
City.javapackage 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
City.javaSource code
CountryService.java
CountryService.javapackage 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
CountryService.javaVaadin generates this interface for City:
Source code
City.ts
City.tsinterface City {
name?: string;
country?: string;
}
export default City;City.ts
City.tsHow 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
AbortSignalthat cancels the request. A cancelled call rejects with aDOMExceptionnamedAbortError. OneAbortControllercan 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=/apiThe 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
src/main/frontend/connect-client.tsimport { 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 asusername. 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 | 400 |
|
An argument can’t be converted or fails validation. | 400 |
|
The call has the wrong number of arguments. | 400 |
|
Access is denied to a user who isn’t logged in, or the request has no valid CSRF token. | 401 |
|
Access is denied to a logged-in user. | 403 |
|
The service or the method doesn’t exist. | 404 |
|
The method throws another exception, or its return value is an unexpected | 500 |
|
The request is cancelled. | — |
|
The server can’t be reached. | — |
|
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
detailargument 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;
}
}