Intro to Hilla: The full-stack Java framework

Based on Vaadin Fusion, Hilla combines a reactive JavaScript front end with a Spring Java back end for full-stack web development. Let's get started with Hilla.

programming

Hilla combines a Java back end built on Spring with a TypeScript front end built with Lit, a fast, reactive framework for JavaScript. Hilla, which is based on Vaadin Fusion, is a unique animal in the Java ecosystem: something akin to Next.js for JavaScript, but with a Spring-based Java back end. This article gets you started with Hilla, including how to scaffold a basic web application, build the front end, and add new components.

Hilla and Vaadin

Vaadin's developers announced in January this year that they were renaming Vaadin Fusion to Hilla. For developers already familiar with Fusion, only the name has changed. For developers just discovering Hilla, you will notice that the examples in this article use packages and components named for Vaadin. Vaadin Fusion packages will be renamed for Hilla in a future release.

Java web development with a reactive front end

Hilla brings together a reactive JavaScript front end and a Spring Java back end in one unified build. The examples in this article show you how these components work together to make Hilla a full-stack framework. In order to follow along, you’ll need both Node.js (npm) and a recent JDK installed on your system. Make sure that both node -v and java –version work!

To get started, open your command line and scaffold a new project via npx, as shown in Listing 1.

Listing 1. Scaffolding a new project in Hilla

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

Now, cd into the new directory, and type ./mvnw (or mvnw for Windows). This command initiates the Maven build. You’ll see output logging for both the back end and front end being built. Soon, the app will be up and running in dev mode. 

hilla sample IDG

Figure 1. Visit localhost:8080, and you should see your Hilla application up and running.

If you take a look at the file system that you just built, you'll see the structure is divided into a standard Maven structure and a front-end directory:

  • /project-root
    • /frontend
      • html
      • ts
      • ts
      • /stores
      • /themes
      • /views
    • /src
    • /target

The project root contains the Maven build file (pom.xml) which builds the Java code from /src into /target, and calls out to the JavaScript build tool (vite.js) to build the front-end application contained in /frontend.

Build the front end

In Hilla, the front end is bootstrapped from the /front-end/index.html, /front-end/index.ts, and routes.ts files. Together, these files determine the routing and set the content to the page for the given route. The most instructive of these pages is routes.ts, seen in Listing 2.

Listing 2. Routes.ts

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[] = [
 // place routes below (more info https://vaadin.com/docs/latest/fusion/routing/overview)
 {
   path: '',
   component: 'hello-world-view',
   icon: '',
   title: '',
 },
 {
   path: 'hello',
   component: 'hello-world-view',
   icon: 'la la-globe',
   title: 'Hello World',
 },
 {
   path: 'about',
   component: 'about-view',
   icon: 'la la-file',
   title: 'About',
   action: async (_context, _command) => {
     await import('./views/about/about-view');
     return;
   },
 },
];
export const routes: ViewRoute[] = [
 {
   path: '',
   component: 'main-layout',
   children: [...views],
 },
];

The code in Listing 2 associates a path with a component. Like many JavaScript frameworks, Hilla is using a component to represent a view. In this case, when the user goes to the blank route, it’ll serve up the hello-world-view component. (Notice that other routes supply additional information like an icon, title, and action.)

The main layout is handled by /frontend/views/main-layout.ts, while the content of hello-world-view is found in /frontend/views/helloworld/hello-world-view.ts, shown in Listing 3. 

Listing 3. hello-world-view.ts

import '@vaadin/button';
import '@vaadin/notification';
import { Notification } from '@vaadin/notification';
import '@vaadin/text-field';
import { html } from 'lit';
import { customElement } from 'lit/decorators.js';
import { View } from '../../views/view';
@customElement('hello-world-view')
export class HelloWorldView extends View {
 name = '';
 connectedCallback() {
   super.connectedCallback();
   this.classList.add('flex', 'p-m', 'gap-m', 'items-end');
 }
 render() {
   return html`
     <vaadin-text-field label="Your name" @value-changed=${this.nameChanged}></vaadin-text-field>
     <vaadin-button @click=${this.sayHello}>Say hello</vaadin-button>
   `;
 }
 nameChanged(e: CustomEvent) {
   this.name = e.detail.value;
 }
 sayHello() {
   Notification.show(`Hello ${this.name}`);
 }
}

The code in Listing 3 shows Lit building the view. If you are familiar with reactive JavaScript idioms, the source should be pretty clear. If not, see my recent introduction to Lit. The render() method is responsible for outputting the contents of the view. We’ll use it here as a place to explore things. In particular, we want to see how to connect this front end with our back end Java endpoints.

Create the Java endpoints

Hilla is built on top of Spring Boot, so we can build out the endpoints using Spring Web as normal. Hilla offers additional functionality to autogenerate the TypeScript that will be consumed in the Lit front end.

Start by creating a new file in /src/java/main/com/example/application called MyEndpoint.java. Paste the contents of Listing 4 into this file.

Listing 4. MyEndpoint.java

package com.example.application;

import com.vaadin.flow.server.auth.AnonymousAllowed;
import dev.hilla.Endpoint;
import dev.hilla.Nonnull;

@Endpoint
@AnonymousAllowed
Public @Nonnull class MyEndpoint {
   public String foo() {
       return "bar";
   }
}

Hilla's @Endpoint annotation tells the framework this class is a REST API. The class is also annotated with the @AnonymousAllowed annotation because Hilla secures all endpoints via Spring security by default. The @Nonnull annotation generates the proper type binding for the front-end TypeScript.

After saving this class file, you can observe that Hilla has generated a new TypeScript file into /frontend/generated/MyEndpoint.ts. We'll use this module to hit the endpoint from the view.

Note: Don’t make changes in these generated files; Hilla will overwrite them based on changes to the Java file.

Now, return to frontend/views/helloworld/hello-world-view.ts, where we'll put our simple endpoint to work. In this case, we want to output the contents of calling the foo() endpoint (which is “bar”). Listing 5 shows the additions you should make on the hello-world-view.ts file. (Note that I've ve removed most of the previous code and left only the additions for this listing.) 

Listing 5. Hello-world-view.ts

//...
import { customElement,property } from 'lit/decorators.js';
import { foo } from 'Frontend/generated/MyEndpoint';

@customElement('hello-world-view')
export class HelloWorldView extends View {
  //...
  @property()
  myString: string = "";
  constructor() {
    super();
    this.getString();
  }
  render() {
     return html`
     //...
     <div>${this.myString}</div>
   `;
 }

  async getString() {
     this.myString = await foo();
   }
}

The main point here is to import the foo() function from the MyEndpoint module, then use it to make a call against the remote back-end Java method we defined earlier.

To do this, we define a reactive property on the class with the Lit TypeScript annotation @property, with the name string. We’ll use this property to store the value from the server. To populate it, we call the async getString() method, which simply calls the foo() function, and puts the return value into myString

Hilla handles most of the work, including making the remote fetch, so we don’t have to think much about it.

Using Vaadin components in Hilla

As I noted previously, Hilla is Vaadin Fusion, so applications built with Hilla can can take advantage of all the well-designed components you might know from that framework. As an example, let’s use a Vaadin grid component to load a collection of novels with their titles and authors.

First, we'll create a model object, which simply holds two Strings, as shown in Listing 6. This file is a typical Java data object. Save it as /src/main/java/com/example/application/Novel.java

Listing 6. A model object for storing novels

package com.example.application;
import javax.validation.constraints.NotBlank;
public class Novel {
   @NotBlank
   private String title;
   @NotBlank
   private String author;
   public Novel(String title, String author){
     this.title = title;
     this.author = author;
   }
   public String getTitle() {
       return title;
   }
   public void setTitle(String title) {
       this.title = title;
   }
   public String getAuthor() {
       return author;
   }
   public void setAuthor(String author) {
       this.author = author;
   }
}

In Listing 7, we serve a List of novels from MyEndpoint.

Listing 7. MyEndpoint with a list of my favorite novels

package com.example.application;
import java.util.*;
import java.util.ArrayList;
import java.util.List;
import com.vaadin.flow.server.auth.AnonymousAllowed;
import dev.hilla.Endpoint;
import dev.hilla.Nonnull;
@Endpoint
@AnonymousAllowed
public class MyEndpoint {
   private final List<Novel> novels = new ArrayList<Novel>();
   MyEndpoint(){
     Novel empireFalls = new Novel("Empire Falls", "Richard Russo");
     Novel reservationBlues = new Novel("Reservation Blues", "Sherman Alexie");
     Novel theAthenianMurders = new Novel("The Athenian Murders", "José Carlos Somoza");
     this.novels.add(empireFalls);
     this.novels.add(reservationBlues);
     this.novels.add(theAthenianMurders);
   }

   public @Nonnull List<Novel> getNovels() {
     return this.novels;
   }
}

In Listing 7, we prepared a few novels with their authors and stuck them into the novels property. We then exposed the data in the getNovels() endpoint.

Now, let's display the new data, as seen in Listing 8. (Note that Listing 8 only shows the changed parts of the code.) 

Listing 8. Use a grid to display the novels

//...
import { foo, getNovels } from 'Frontend/generated/MyEndpoint';
import '@vaadin/grid/vaadin-grid';
@customElement('hello-world-view')
export class HelloWorldView extends View {
 @property()
 novels: object = {};
 constructor() {
   //...
   this.initNovels();
 }
 render() {
   return html`
     <vaadin-grid .items="${this.novels}" theme="row-stripes">
       <vaadin-grid-column path="title"></vaadin-grid-column>
       <vaadin-grid-column path="author"></vaadin-grid-column>
     </vaadin-grid>
   `;
 }
  async initNovels(){
     this.novels = await getNovels();
}

In this listing, we import the getNovels object from frontend/generated/MyEndpont, which Hilla generated for us. Then, we use that method as the source for the contents of this.novels.

Next, we use this.novels to provide the .items property to the imported vaadin-grid component. The end result is a nicely formatted grid component with minimal effort.

Conclusion

This article introduced Hilla, a full-stack framework based on Vaadin Fusion. Hilla offers a nicely integrated experience for building Java web applications with a reactive front end. Thanks to Vaadin, it has many useful components ready to use. The examples in this article should give you a feeling for working with Hilla.

Copyright © 2022 IDG Communications, Inc.

How to choose a low-code development platform