Add a Service
In this guide, you’ll learn how to make a Java service browser-callable and how to call it from a Hilla view. This guide covers the basics to get you started. For more details, see the Hilla Endpoints Reference Guide.
Making a Service Browser-Callable
To call a Java service from Hilla, you must first make it browser-callable. When a service is browser-callable, Vaadin creates a server endpoint and generates a TypeScript client for it. This allows you to call the Java service from the browser in a type-safe way, without having to create a REST controller for it. Additionally, the generated client handles the endpoint URL automatically, eliminating the need for manual configuration.
To make a Java service browser-callable, annotate it with @BrowserCallable
:
@BrowserCallable
@AnonymousAllowed
public class CounterService {
public int addOne(int number) {
return number + 1;
}
}
Vaadin protects browser-callable services by default to prevent unauthorized access. This means you must always use a security annotation like @AnonymousAllowed
or @RolesAllowed
to explicitly define access control. Refer to the Protect Browser-Callable Services guide for more details.
When is the TypeScript Client Generated?
Vaadin generates TypeScript clients on the following occasions:
-
When the application starts up in development mode
-
When a browser-callable service is hotswapped
-
When Spring DevTools reloads the application
-
During a production build
If needed, you can manually trigger the TypeScript client generation using Maven:
./mvnw compile vaadin:generate
Note
| The generator analyzes Java byte code, which is why you need to compile your Java classes before you run the generator. |
Calling a Service
You can import TypeScript clients from the src/main/frontend/generated/endpoints.ts
file.
The following example calls the CounterService
when the user clicks a button:
import { Button } from "@vaadin/react-components";
import { useSignal } from "@vaadin/hilla-react-signals";
import { CounterService } from "Frontend/generated/endpoints";
export default function CounterView() {
const number = useSignal(0); // Initialize state with 0
const addOne = async () => {
number.value = await CounterService.addOne(number.value);
};
return (
<main>
<div>{number}</div>
<Button onClick={addOne}>Add One</Button>
</main>
);
}
TypeScript client functions are always async
, so you must use the await
keyword or handle the returned Promise
.
Input & Output
A browser-callable service can return and receive as input both primitive types and non-primitive types. When a non-primitive type (such as an object) is used, Vaadin automatically generates a corresponding TypeScript interface.
You can use both Java classes and records as input and output. They can be standalone classes, inner classes of the service itself, or come from a project dependency.
Tip
|
Prefer Records to Classes
If you’re unsure whether to use classes or records for input and output objects, opt for records. Records are preferred over classes because they are more concise and immutable by default, reducing boilerplate code.
|
Vaadin uses Jackson for object serialization between Java and JSON. You can use some Jackson annotations to customize the generated TypeScript interface as well. See the Type Conversion Reference Guide for details.
For example, consider the following Java code:
public record City ( 1
String name,
String country
) {}
-
This is a standalone record (which could also be a class).
@BrowserCallable
@AnonymousAllowed
public class CityService {
public List<City> findCities(Query query) {
// ...
}
public static class Query { 1
private final int numberOfCities;
public Query(final int numberOfCities) {
this.numberOfCities = numberOfCities;
}
public int getNumberOfCities() {
return numberOfCities;
}
}
}
-
This is an inner class.
The generated TypeScript interfaces for City
and Query
would look like this:
interface City {
name?: string;
country?: string;
}
export default City;
interface Query {
numberOfCities: number;
}
export default Query;
Nullable & Non-Nullable Types
In TypeScript, attributes can be either nullable or non-nullable. Vaadin follows this rule when generating TypeScript interfaces:
-
Primitive types (e.g.,
int
,double
,boolean
) are non-nullable by default. -
Reference types (e.g.,
String
,UUID
,LocalDate
) are nullable by default.
If you look at the earlier examples, you’ll see that numberOfCities
is non-nullable, whereas name
and country
are both nullable. This is because numberOfCities
is a primitive type (int
) and the others are reference types (String
).
You can force a reference type to be generated as non-nullable by using the JSpecify @NonNull
annotation. You can control nullability in other ways as well; see the Type Nullability Reference Guide for details.
Note
| Starting from version 24.7, Vaadin includes JSpecify as a transitive dependency. |
For example, to make name
and country
non-nullable, you’d do this:
import org.jspecify.annotations.NonNull;
public record City(
@NonNull String name,
@NonNull String country
) {}
The generated TypeScript interface would then look like this:
interface City {
name: string;
country: string;
}
export default City;
The fields are no longer marked as nullable (i.e., the ?
is missing).
Service Methods
The nullability rules apply to input parameters and return values of browser-callable service methods as well. For example, consider the following service:
@BrowserCallable
@AnonymousAllowed
public class CityService {
public List<City> findCities(Query query) {
// ...
}
}
The generated TypeScript function would look like this:
async function findCities_1(
query: Query | undefined,
init?: EndpointRequestInit_1
): Promise<Array<City | undefined> | undefined> {
// ...
}
By default, the query parameter, the returned array, and its elements are all nullable, which may not always be desirable. To make everything non-nullable, you’d have to annotate all three items, like this:
@BrowserCallable
@AnonymousAllowed
public class CityService {
public @NonNull List<@NonNull City> findCities(@NonNull Query query) {
// ...
}
}
The generated TypeScript function would then look like this:
async function findCities_1(
query: Query,
init?: EndpointRequestInit_1
): Promise<Array<City>> {
// ...
}
Tip
|
Change the Default Nullability
If most types in your project should be non-nullable by default, apply Spring’s @NonNullApi annotation at the package level in package-info.java . This makes all types in the package non-nullable unless explicitly marked as @Nullable .
|