Fetching Data from Server with Hilla Endpoints
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 com.example.application.data.entity
package.

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 com.example.application.data.repository
package.
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, CrmEndpoint.java
, in the com.example.application.data.endpoint
package with the following content:
package com.example.application.data.endpoint;
import java.util.Collections;
import java.util.List;
import com.example.application.data.entity.Company;
import com.example.application.data.entity.Contact;
import com.example.application.data.entity.Status;
import com.example.application.data.repository.CompanyRepository;
import com.example.application.data.repository.ContactRepository;
import com.example.application.data.repository.StatusRepository;
import com.vaadin.flow.server.auth.AnonymousAllowed;
import dev.hilla.Endpoint;
import dev.hilla.Nonnull;
@Endpoint
@AnonymousAllowed
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 {
@Nonnull
public List<@Nonnull Contact> contacts = Collections.emptyList();
@Nonnull
public List<@Nonnull Company> companies = Collections.emptyList();
@Nonnull
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.
@Nonnull
public CrmData getCrmData() {
CrmData crmData = new CrmData();
crmData.contacts = contactRepository.findAll();
crmData.companies = companyRepository.findAll();
crmData.statuses = statusRepository.findAll();
return crmData;
}
@Nonnull
public Contact saveContact(Contact contact) {
contact.setCompany(companyRepository.findById(contact.getCompany().getId())
.orElseThrow(() -> new RuntimeException(
"Could not find Company with ID " + contact.getCompany().getId())));
contact.setStatus(statusRepository.findById(contact.getStatus().getId())
.orElseThrow(() -> new RuntimeException(
"Could not find Status with ID " + contact.getStatus().getId())));
return contactRepository.save(contact);
}
public void deleteContact(Long contactId) {
contactRepository.deleteById(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.