Fetching Data from Server with Hilla Endpoints

Hilla endpoints are a type-safe alternative to REST. Call the server with async TypeScript methods and get compile-time type checking.

One of the unique features of Hilla is type-safe server access through endpoints. Instead of using REST and JSON, Hilla gives you type-safe asynchronous server access.

Annotating a class with @Endpoint makes the methods available to the TypeScript frontend as async methods. Hilla generates TypeScript types based on the Java method parameters and return types, so you have type safety throughout your application.

You can find the back-end code in the src/main/java directory.

This chapter covers:

  • the back-end architecture,

  • how to create an endpoint,

  • how to optimize communication by using data transfer objects (DTO).

Overview of the Backend

The starter you downloaded contains the entities and repositories you need, along with a sample data generator.

Domain Model: Entities

The Hilla CRM application has three JPA entities that make up its domain model: Contact, Company, and Status. A contact belongs to a company and has a status.

You can find the entities in the package.

Entity model diagram

Database Access: Repositories

The application uses Spring Data JPA repositories for database access. Spring Data provides implementations of basic create, read, update, and delete (CRUD) database operations when you extend from the JpaRepository interface.

You can find the repositories in the package.

Sample Data Generator

The package contains a sample data generator that populates the database with data. It uses CommandLineRunner, which Spring Boot runs when the application starts up.

Service Layer

In a more complex application, you want to also have a service layer that runs business logic, coordinates data layer access, and maps entities to data transfer objects to decouple entities from business objects.

This tutorial omits the service layer for the sake of simplicity.

Creating a Hilla Endpoint

You can turn a class into an endpoint by adding an @Endpoint annotation to it. When you create an endpoint, Hilla:

  • exposes secured REST endpoints for each method,

  • generates TypeScript type definitions for method parameters and return types, and

  • creates asynchronous TypeScript methods to call the endpoints in a type-safe manner.

Creating the CRM Endpoint

For this application, you need an endpoint that:

  • provides all contacts, companies, and statuses in the database, and

  • lets you save and delete contacts.

Create a new file,, in the package with the following content:


import java.util.Collections;
import java.util.List;
import com.vaadin.flow.server.auth.AnonymousAllowed;
import dev.hilla.Endpoint;
import dev.hilla.Nonnull;

public class CrmEndpoint {

Turn the class into an endpoint with @Endpoint and allow anonymous access with @AnonymousAllowed. The tutorial covers authentication and securing endpoints later, in the Adding a login screen and authenticating users chapter.

Inject the required repositories into the endpoint class through the constructor. Hilla endpoints are Spring components, so you can use the Spring dependency injection container as you would for any other Spring components. Save the repositories in fields for later access.

private ContactRepository contactRepository;
private CompanyRepository companyRepository;
private StatusRepository statusRepository;

public CrmEndpoint(ContactRepository contactRepository, CompanyRepository companyRepository,
  StatusRepository statusRepository) {
  this.contactRepository = contactRepository;
  this.companyRepository = companyRepository;
  this.statusRepository = statusRepository;

Use a data transfer object to wrap the contacts, companies, and statuses into one return type. This way, the client only needs to make one server call to get all the required data.

Within the same class, create an inner class, CrmData. Because this class is only used as a data wrapper, it doesn’t need data encapsulation and the associated getters and setters. Instead, it uses public fields.

public static class CrmData {
  public List<@Nonnull Contact> contacts = Collections.emptyList();
  public List<@Nonnull Company> companies = Collections.emptyList();
  public List<@Nonnull Status> statuses = Collections.emptyList();

TypeScript is stricter about handling null values than Java is. Because of this, Hilla generates optional (nullable) TypeScript types for all non-primitive Java types. Hence, you need to ensure that you never return null values or collections with null elements. You do this by annotating the types with @Nonnull. This creates non-nullable TypeScript types that are easier to work with. You can read more about type nullability in the Type nullability article.

Next, implement API methods to get, update, and delete data.

public CrmData getCrmData() {
  CrmData crmData = new CrmData();
  crmData.contacts = contactRepository.findAll();
  crmData.companies = companyRepository.findAll();
  crmData.statuses = statusRepository.findAll();
  return crmData;

public Contact saveContact(Contact contact) {
      .orElseThrow(() -> new RuntimeException(
          "Could not find Company with ID " + contact.getCompany().getId())));
      .orElseThrow(() -> new RuntimeException(
          "Could not find Status with ID " + contact.getStatus().getId())));

public void deleteContact(Long contactId) {

The saveContact() method looks up the company and status by ID to avoid saving changes to them by accident.

Save the file and confirm that the development server build is successful. If you have shut down the server, restart it with the ./mvnw command from the command line.