BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage Articles How to Manage Full-Stack Java Development with Hilla

How to Manage Full-Stack Java Development with Hilla

Bookmarks

Key Takeaways

  • Hilla is an open-source framework that promises a significant increase in efficiency in the development of web applications.
  • It integrates a Spring Boot Java backend with a reactive TypeScript frontend. 
  • User interfaces are created using Lit or React and Vaadin’s 40+ open-source UI web components.  
  • Hilla helps build business apps faster with type-safe server communication and integrated tooling. 
  • Hilla also automatically generates the REST API and the access code for the client. 
  • The backend is secure by default and completely stateless.

Hilla stands out in the open-source community as a framework designed to streamline web application development. Its combination of a Spring Boot Java backend with a reactive TypeScript frontend, along with UI design through Lit or React, enables the creation of dynamic applications. It's further enhanced by Vaadin’s 40+ open-source UI web components, providing ready-to-use elements for a superior user experience.

Hilla takes efficiency and security seriously, automatically generating the API and client access code, and ensuring a secure backend by default. This article will delve into Hilla's core aspects: its use of Lit, Spring Bean endpoints, front and backend personas, and routing views. These insights will help developers leverage Hilla to build robust business applications more swiftly.

Here are several examples of how Hilla delivers increased developer efficiency via Lit, Spring Bean endpoints, frontend and backend personas, and routing views.

Hilla

The Hilla framework is developed by the Finnish company Vaadin, which also maintains the eponymous Java web framework Vaadin Flow.

Unlike Vaadin Flow, which uses a pure Java approach, Hilla is a classic single-page application (SPA) framework focusing on full-stack development.

This means that the client is developed in TypeScript. Either the Lit framework or React can be used in the front end, and currently, only Spring Boot is used in the backend, but work is being done to support other Java frameworks.

A Hilla project is a pure Maven project. Under the hood, the Hilla Maven plugin uses npm and Vite for front-end building.

Unlike traditional frontend development, however, you don't have to worry about configuring and running these tools, which significantly simplifies starting with frontend development, especially for Java developers.

Lit

Hilla supports Lit and React on the client side. I’ll focus on Lit in this article because it was the first client framework used in Hilla. Lit is the successor to the well-known Polymer library [Polymer] and is used to develop Web Components quickly. With Lit, so-called custom components, i.e., extensions of the HTML language, can be developed. The templates are declaratively included in the TypeScript code, and CSS, which is only valid within the context of the web component, can also be added. The properties of the web component are reactive and automatically re-render when changes occur.

@customElement('simple-greeting')
export class SimpleGreeting extends LitElement {
 
    @property()
    name?: string = 'World';
 
    render() {
        return html`<p>Hello, ${this.name}!</p>`;
    }
}

Code Image 1: Component with Lit

The critical thing to note in Image 1 is the name in the @customElement decorator, which must include a hyphen to distinguish it from a standard HTML element. The @property decorator makes the string name a reactive property, which can be set from outside the component and causes the component to be re-rendered when changed. The render() method generates the template for the web component. In the generated DOM, the component can be found as shown in Image 2.

<body>
    <simple-greeting name="World"></simple-greeting>
</body>

Code Image 2:  Rendered Webcomponent

Endpoints

On the backend side, Hilla uses so-called endpoints. An endpoint is a Spring Bean annotated with @Endpoint. From this, Hilla generates a REST API, including TypeScript code, for accessing it on the client side.

@Endpoint
@AnonymousAllowed
public class HelloWorldEndpoint {

    @Nonnull
    public String sayHello(@Nonnull String name) {
            if (name.isEmpty()) {
            return "Hello stranger";
            } else {
            return "Hello " + name;
            }
    }
}

Code Image 3: Endpoint

The first thing to notice in Image 3 is the @AnonymousAllowed annotation. This annotation is necessary to access the API without authentication, as all endpoints in Hilla are protected by default. The @Nonnull annotation should also be noted. Since TypeScript is more strict with nulls than Java, this can inform the TypeScript generator that both the parameter and the return value should never be null.

function _sayHello(name: string): Promise<string> {
   return client.call('HelloWorldEndpoint', 'sayHello', {name});
}
export { _sayHello as sayHello };

Code Image 4: Generated TypeScript Code

Image 4 shows the generated TypeScript code that can be used in the frontend. The code is regenerated if anything changes in the endpoint, parameter, or return types, and an appropriate error is reported on the client side. This helps to detect errors in the usage of the API during development.

The Example Application

The application will display a table of personal data, which can be edited using a form. The personal data will be stored in a database using JPA. Figure 1 shows what the result will look like. The example code is published on GitHub.

Figure 1: Grid with Form

CLI

Before creating a Hilla application, developers need to install NodeJS version 16.14 or newer. After that, the Vaadin CLI can be used with npx to create a new project. The CLI generates a complete Hilla application with a Hello-World-View and the HelloWorldEndpoint from Image 3.

npx @vaadin/cli init --hilla hilla-app

Code Image 5:  CLI

Backend

First, an entity named Person is added. The example uses JPA to persist the data in an H2 database.

@Entity
public class Person {

    @Id @GeneratedValue
    private Long id;


    @NotBlank
    private String firstName;

    @NotBlank
    private String lastName;

    @Email @NotBlank
    private String email;
   
    ...
}

Code Image 6: Person Entity

As shown in Image 6, Jakarta Bean validation annotations are used. These are also taken into account by the Hilla generator. If the Person entity is used in a form in the client, the inputs are validated according to the annotations (Figure 2).

Figure 2: Validation

As the next step, the endpoint is created to read and save the data of the persons. The PersonRepository used in Image 7 extends the Spring Data JPA JpaRepository interface.

@Endpoint
@AnonymousAllowed
public class PersonEndpoint {

    @Autowired
    private PersonRepository personRepository;

    @Nonnull
    public List<@Nonnull Person> findAll() {
        return personRepository.findAll();
    }
 
    public void save(@Nonnull Person person) {
        this.personRepository.save(person);
    }
}

Code Image 7: Person Endpoint

public interface PersonRepository extends JpaRepository<Person, Integer> {
}

Code Image 8:  Person Repository

Frontend

Displaying Persons

On the client side, a view is needed to display the person data, which uses a Vaadin grid. All Vaadin components are web components and can therefore be easily used with Lit. The Vaadin grid offers paging, sorting, and many other functions, making displaying data in table form very easy.

@customElement('person-view')
export class PersonView extends View {

    @state()
    people: Person[] = [];

    async connectedCallback() {
            super.connectedCallback();
            this.people = await PersonEndpoint.findAll();
    }

    render() {
        return html`
            <vaadin-grid .items=${this.people} style="height: 100%">
                <vaadin-grid-column path="firstName"></vaadin-grid-column>
                <vaadin-grid-column path="lastName"></vaadin-grid-column>
                <vaadin-grid-column path="email"></vaadin-grid-column>
            </vaadin-grid>
        `;
    }
}

Code Image 9: Person View

In the connectedCallback method, which is called when the web component is added to the DOM, the person entities are read from the endpoint (Image 9). The persons are added to the Vaadin Grid's items property, and the "path" property is used to define the path to the person's property. For simplicity, this example does not use paging. If the table contains a larger number of records, paging should be used to load a subset of the data. Hilla offers a DataProvider for this purpose, which provides information about the currently displayed page, page size, and selected sorting, and requests data from the endpoint page by page when paging. A detailed code example can be found in the GitHub repository.

Editing Persons

Editing person data requires the creation of a form. To do this, Vaadin web components are used, as shown in Image 10.

<vaadin-form-layout>
    <vaadin-text-field
            label="First name"
            ${field(this.binder.model.firstName)}
    ></vaadin-text-field>
    <vaadin-text-field
            label="Last name"
            ${field(this.binder.model.lastName)}
    ></vaadin-text-field>
    <vaadin-text-field
            label="Email"
            ${field(this.binder.model.email)}
    ></vaadin-text-field>
</vaadin-form-layout>
<vaadin-button @click=${this.save}>Save</vaadin-button>

Code Image 10: Form

To bind a Person entity to the components, Hilla provides a binder (Image 11). The binder uses the generated PersonModel class, which contains additional information about the Person entity, such as validation or type.

private binder = new Binder<Person, PersonModel>(this, PersonModel);

Code Image 11: Binder

To be able to save the changed Person entity, we extend the PersonEndpoint with the method save. This method can be directly passed to the binder. For this purpose, the click event is bound to the button (see Image 10), and the save method is called. After saving, the person's data is reloaded, which updates the grid (Image 12).

private async save() {
    await this.binder.submitTo(PersonEndpoint.save);
    this.people = await PersonEndpoint.findAll();
}

Code Image 12: Save Method

Now, all that is left is passing the selected person from the grid to the binder. For this purpose, the active-item-changed event can be used (see Image 13). Also, the grid needs to be informed about which person is selected, which is done using the selectedItems property.

<vaadin-grid
        .items=${this.people}
        @active-item-changed=${this.itemSelected}
        .selectedItems=${[this.selectedPerson]}>

Code Image 13: Grid-Selection

Now, in the itemSelected method in Image 14, only the selected person needs to be read from the event and passed to the binder. This will populate the form.

private async itemSelected(event: CustomEvent) {
    this.selectedPerson = event.detail.value as Person;
    this.binder.read(this.selectedPerson);
}

Code Image 14: itemSelected Method

Routing

If the application includes more than one view, then we will need a way to navigate between the views. Hilla uses the Vaadin router for this purpose (Image 15). First, the view that is displayed when the application starts up, in this case, the hello-world-view, is imported. It is then mapped to both the root path and the path hello-world. In the example the master-detail-view, the other view is loaded lazily so it is only loaded when the user navigates to it. Finally, a layout is defined for the views, which includes elements such as a header and footer, as well as the navigation component.

import {Route} from '@vaadin/router';
import './views/helloworld/hello-world-view';
import './views/main-layout';
 
export type ViewRoute = Route & {
    title?: string;
    icon?: string;
    children?: ViewRoute[];
};
 
export const views: ViewRoute[] = [
    {
        path: '',
        component: 'hello-world-view',
        icon: '',
        title: '',
    },
    {
        path: 'hello-world',
        component: 'hello-world-view',
        icon: 'la la-globe',
        title: 'Hello World',
    },
    {
        path: 'master-detail',
        component: 'master-detail-view',
        icon: 'la la-columns',
        title: 'Master-Detail',
        action: async (_context, _command) => {
            await import('./views/masterdetail/master-detail-view');
            return;
        },
    },
];
export const routes: ViewRoute[] = [
    {
        path: '',
        component: 'main-layout',
        children: [...views],
    },
];

Code Image: 15 Router Configuration

Deployment in Production

By default, Hilla applications are configured to run in development mode. This requires slightly more memory and CPU performance but allows for easier debugging. For deployment, the application must be built in production mode. The main difference between development and production mode is that in development mode, Hilla uses Vite to deliver JavaScript files to the browser instead of to the Java server on which the application is running. When a JavaScript or CSS file is changed, the changes are considered and automatically deployed. In production mode, however, it is more efficient to prepare JavaScript and CSS files once during building and let a server handle all requests. At the same time, client resources can be optimized and minimized further to reduce the network and browser load.

The pom.xml file in a Hilla project uses a profile with the configuration of the Vaadin plugin to create a build in production mode (Image 16).

<profiles>
    <profile>
        <id>production</id>
        <build>
            <plugins>
                <plugin>
                    <groupId>dev.hilla</groupId>
                    <artifactId>hilla-maven-plugin</artifactId>
                    <version>${hilla.version}</version>
                    <executions>
                        <execution>
                            <goals>
                                <goal>build-frontend</goal>
                            </goals>
                            <phase>compile</phase>
                        </execution>
                    </executions>
                    <configuration>
                        <productionMode>true</productionMode>
                    </configuration>
                </plugin>
            </plugins>
        </build>
    </profile>
</profiles>

Code Image 16: Maven Plugin

To create a production build, developers can invoke Maven as illustrated in Image 17. This process will generate a JAR file that includes all dependencies and transpiled front-end resources, ready for deployment.

./mvnw package -Pproduction

Code Image 17:  Production Build

Conclusion

Because Hilla automatically generates the access code to the endpoints and model classes, it makes integrating the frontend and backend much easier than with traditional Single Page Application development. The included Vaadin web components, such as the grid, are also extremely helpful in developing data-intensive applications. The binder, especially in combination with Bean validation, makes it very easy to create forms and reduces the code to a minimum. As the developer does not have to deal with frontend build and tools, Hilla is also very suitable for Java developers. Overall, these features enable Hilla to deliver increased efficiency for applications that combine a reactive frontend with a Java backend.

The article only covered the most critical aspects of Hilla. Hilla provides several other capabilities to create a fully featured application, such as styling and theming, security, localization, error handling, or application-wide state management. The official documentation covers these and many other topics.

About the Author

Rate this Article

Adoption
Style

BT