> Markdown version of [Basics Tutorial](https://vaadin.com/docs/v24/hilla/lit/start/basics). Section index: [llms.txt](https://vaadin.com/docs/v24/hilla/llms.txt)

# Basics Tutorial

## <a id="requirements"></a>1. Requirements

- About 10–20 minutes, depending on if you decide to do every part step by step.

  1. [Node](https://nodejs.org/) 18.0 or later.

  2. JDK 17 or later, for example, [Eclipse Temurin JDK](https://adoptium.net/).

[Visual Studio Code](https://code.visualstudio.com/) is used in this tutorial. See the [setup instructions](https://youtu.be/G_aJONwi0qo) on YouTube. You can use any IDE that supports Java, TypeScript, HTML, and CSS.

This tutorial is intended for developers with a basic understanding of Java, JavaScript, HTML, and CSS. You don’t need to be an expert by any means, but understanding the syntax and basic concepts makes it easier to follow along.

## <a id="create-a-hilla-project"></a>2. Create a Hilla Project

Use the Hilla CLI to create a new project:

```terminal
npx @hilla/cli init --preset hilla-tutorial hilla-todo
```

Alternatively, you can [download the starter as a zip-file](https://start.vaadin.com/dl?preset=hilla-tutorial\&projectName=hilla-todo) and extract it.

Import the project in the IDE of your choice. If you downloaded the application as a ZIP, extract it.

If you open one of the `.ts` files inside the `frontend/views` folder, you might see red underlines for unresolved imports. They get resolved when you start the development server for the first time, when all dependencies are downloaded.

Importing into VS Code

Import the project by either:

- navigating to the project folder and running `code .` (note the period), or

- choosing **File** **›** **Open…​** in VS Code and selecting the *project folder*.

##### <a id="recommended-extensions"></a>Recommended Extensions

You should install the following extensions to VS Code for an optimal development experience:

- [Java Extension Pack](https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-java-pack)

- [Lit-plugin](https://marketplace.visualstudio.com/items?itemName=runem.lit-plugin)

- [Spring Boot Tools](https://marketplace.visualstudio.com/items?itemName=Pivotal.vscode-spring-boot)

VS Code should automatically suggest these for you when you open the project.

Project architecture and structure

Hilla projects are based on [Spring Boot](https://spring.io/projects/spring-boot) and use [Maven](https://maven.apache.org/) for dependency management and build configuration.

[Image: Project structure]

The following lists the key folders and files in a Hilla application project:

- `frontend`

  The folder where your views and frontend code live.

- `src`

  The folder where your Java backend code lives.

- `pom.xml`

  The project configuration file. Defines dependencies.

- `frontend/index.html`

  The bootstrap page. You don’t usually need to edit this.

- `frontend/index.ts`

  Your application routes are defined here.

- `src/main/java/com/example/application/Application.java`

  The Java class that the Spring Boot application.

See [Application architecture](https://vaadin.com/docs/v24/hilla/lit/guides/architecture.md) for more details.

## <a id="define-the-data-model-service-layer"></a>3. Define the Data Model & Service Layer

Begin by setting up the data model and services for accessing the database. You can do this in two steps:

1. Define an entity.

2. Create a repository for accessing the database.

This tutorial shows how to use an in-memory H2 database and JPA for persistence. The starter you downloaded already includes the needed dependencies in the `pom.xml` file.

### <a id="define-an-entity"></a>3.1. Define an Entity

Define a JPA *entity* class for the data model, by creating a new file, `Todo.java`, in `src/main/java/com/example/application` with the following content:

`Todo.java`

```java
package com.example.application;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.validation.constraints.NotBlank;

@Entity // (1)
public class Todo {

  @Id
  @GeneratedValue
  private Integer id;

  private boolean done = false;

  @NotBlank // (2)
  private String task;

  public Todo() {}

  public Todo(String task) {
    this.task = task;
  }

  public Integer getId() {
    return id;
  }

  public void setId(Integer id) {
    this.id = id;
  }

  public boolean isDone() {
    return done;
  }

  public void setDone(boolean done) {
    this.done = done;
  }

  public String getTask() {
    return task;
  }

  public void setTask(String task) {
    this.task = task;
  }
}
```

1. Turn the class into a JPA entity with an `@Entity` annotation.

2. Add a `@NotBlank` Java bean validation annotation to enforce validity both in the view and on the server.

### <a id="create-a-repository"></a>3.2. Create a Repository

Next, create a *repository* for accessing the database. You only need to define an interface with type information: Spring Data takes care of the implementation. Create a new file, `TodoRepository.java`, in `src/main/java/com/example/application`, with the following contents:

`TodoRepository.java`

```java
package com.example.application;

import org.springframework.data.jpa.repository.JpaRepository;

public interface TodoRepository extends JpaRepository<Todo, Integer> {
}
```

You now have all the necessary backend code in place to start building a UI.

Hilla can generate TypeScript versions of the Java files you created. To achieve this, run the project from the command line:

terminal

**Windows**

```bash
mvnw
```

terminal

**macOS / Linux**

```bash
./mvnw
```

**The first time you run the application, it may take up to a few minutes**, as Hilla downloads all the dependencies and builds a frontend bundle. Subsequent builds don’t download dependencies, so that they are much faster.

When the build has finished, you should see the application running on <http://localhost:8080>.

[Image: Running project]

## <a id="create-a-typed-server-endpoint"></a>4. Create a Typed Server Endpoint

One of the key features of Hilla is type-safe server access through *endpoints*. When you define an `@Endpoint`, Hilla creates the needed REST-like endpoints, secures them, and generates TypeScript interfaces for all the data types and public methods used. Having full-stack type safety helps you stay productive through autocomplete and helps guard against breaking the UI when you change the data model on the server.

Create a new `TodoEndpoint.java` file in `src/main/java/com/example/application` with the following content:

`TodoEndpoint.java`

```java
package com.example.application;

import java.util.List;

import com.vaadin.flow.server.auth.AnonymousAllowed;
import com.vaadin.hilla.Endpoint;
import org.jspecify.annotations.NonNull;


@Endpoint // (1)
@AnonymousAllowed // (2)
public class TodoEndpoint {
  private TodoRepository repository;

  public TodoEndpoint(TodoRepository repository) { // (3)
    this.repository = repository;
  }

  public @NonNull List<@NonNull Todo> findAll() { // (4)
    return repository.findAll();
  }

  public Todo save(Todo todo) {
    return repository.save(todo);
  }
}
```

1. Annotating a class with `@Endpoint` exposes it as a service for client-side views. All **public** methods of an endpoint are callable from TypeScript.

2. By default, endpoint access requires an authenticated user. `@AnonymousAllowed` enables access for anyone. See [Configuring Security](https://vaadin.com/docs/v24/hilla/lit/guides/security/configuring.md) for more information on endpoint security.

3. Use Spring to automatically inject the `TodoRepository` dependency for database access.

4. Using the `@NonNull` annotation ensures that the TypeScript generator doesn’t interpret these values as possibly `undefined`.

## <a id="build-the-todo-view"></a>5. Build the Todo View

Hilla uses the [Lit](https://lit.dev/) library for client-side views. Lit is a lightweight and highly performant library for building reactive components with declarative templates.

Next, you create a view for adding and viewing to-do items. You can choose to build it step by step learning some concepts along the way, or copy the complete view implementation if you are in a hurry. When you’re ready, you can learn more about [creating components](https://vaadin.com/docs/v24/hilla/lit/components/create.md).

Build the View Step-by-Step

Open the `frontend/views/todo/todo-view.ts` file and replace its contents with the following:

`todo-view.ts`

```typescript
import { html } from 'lit';
import { customElement, state } from 'lit/decorators.js';
// (1)
import '@vaadin/button';
import '@vaadin/checkbox';
import '@vaadin/text-field';
import { Binder, field } from '@vaadin/hilla-lit-form';
import Todo from 'Frontend/generated/com/example/application/Todo';
import TodoModel from 'Frontend/generated/com/example/application/TodoModel';
import { TodoEndpoint } from 'Frontend/generated/endpoints';
import { View } from '../view';

@customElement('todo-view') // (2)
export class TodoView extends View { // (3)
}
```

1. Import the Lit web components, helpers, and generated TypeScript models required for building the view.

2. Register the new component with the browser. This makes it available as `<todo-view>`. The routing in `index.ts` is already set up to show it when you navigate to the application.

3. Define the component class that extends from `View`.

### <a id="define-the-view-state"></a>Define the View State

Inside the `TodoView` class, define the view state as follows:

`todo-view.ts`

```typescript
@state()
private todos: Todo[] = []; // (1)
private binder = new Binder(this, TodoModel); // (2)
```

1. The list of `Todo` items is private and decorated with `@state()`, so Lit observes it for changes.

2. A Hilla `Binder` is used to handle the form state for creating new `Todo` objects. `TodoModel` is automatically generated by Hilla. This describes the data types and validations that `Binder` needs. Read more about forms in [Binding Data to Forms](https://vaadin.com/docs/v24/hilla/lit/guides/forms/binder.md).

### <a id="define-styles"></a>Define Styles

Define some padding on the view. Web Components have a default `display` value of `inline`, which is rarely what you want. Set it to `block` instead by adding the following code to the `themes/hilla-todo/styles.css` file, which is where you define styles in a Hilla application, as explained in [Styling](https://vaadin.com/docs/v24/hilla/lit/guides/styling.md).

`themes/hilla-todo/styles.css`

```css
todo-view {
  display: block;
  padding: var(--lumo-space-m) var(--lumo-space-l); /* <1> */
}
```

1. The `padding` property is defined using the [spacing properties](https://vaadin.com/docs/latest/styling/lumo/lumo-style-properties/size-space#space) to be consistent with the rest of the app.

### <a id="define-the-html-template"></a>Define the HTML Template

Go back to the `todo-view.ts` file and define a `render()` method that returns an `html` template literal inside the class.

`todo-view.ts`

```typescript
render() {
  return html`

  `;
}
```

Add the following code within the `html` template:

`todo-view.ts`

```typescript
return html`
<div class="form">
  <vaadin-text-field
    label="Task"
    ${field(this.binder.model.task)/* <1> */}
  ></vaadin-text-field>
  <vaadin-button
    theme="primary"
    @click="${this.createTodo/* <2> */}"
    ?disabled="${this.binder.invalid/* <3> */}"
  >
    Add
  </vaadin-button>
</div>`;
```

1. The Text Field component is bound to the `task` property of a `Todo` using `${field(this.binder.model.task)}`. You can read more about forms in [Binding Data to Forms](https://vaadin.com/docs/v24/hilla/lit/guides/forms/binder.md).

2. The click event of the **Add** button is bound to the `createTodo()` method.

3. The button is disabled if the form is invalid.

Right underneath the previous `<div>`, add the following code:

`todo-view.ts`

```typescript
return html`<div class="todos">
    ${this.todos.map((todo) => /* <1> */html`
        <div class="todo">
          <vaadin-checkbox
            ?checked=${todo.done/* <2> */}
            @checked-changed=${(e: CustomEvent) => // (3)
              this.updateTodoState(todo, e.detail.value)}
          ></vaadin-checkbox>
          <span>${todo.task}</span>
        </div>
      `
    )}
  </div>`;
```

1. The existing `todo` items are shown by mapping the `todos` array to Lit templates. The template for a single `Todo` contains a checkbox and the task text.

2. Bind the `checked` boolean attribute to the `done` property on the `todo`.

3. Call the `updateTodoState()` method, with the `todo` and the new value, whenever the checked value changes.

### <a id="update-view-state-and-call-backend"></a>Update View State and Call Backend

Below the `render()` method in the `TodoView` class, add a `connectedCallback()` lifecycle callback to initialize the view when it’s attached to the DOM.

`todo-view.ts`

```typescript
async connectedCallback() { // (1)
  super.connectedCallback(); // (2)
  this.todos = await TodoEndpoint.findAll(); // (3)
}
```

1. Use an [async function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function) to make it easier to handle asynchronous code.

2. Remember to call the superclass method.

3. The `getTodos()` method is automatically generated by Hilla based on the method in `TodosEndpoint.java`. The method was imported in the head of the file. The `await` keyword waits for the server response without blocking the UI.

Below the `connectedCallback()`, add another method to handle the creation of a new `Todo`.

`todo-view.ts`

```typescript
async createTodo() {
  const createdTodo = await this.binder.submitTo(TodoEndpoint.save); // (1)
  if (createdTodo) {
    this.todos = [...this.todos, createdTodo]; // (2)
    this.binder.clear(); // (3)
  }
}
```

1. Use `binder` to submit the form to `TodoEndpoint`. The `Binder` validates the input before posting it, and the server revalidates it.

2. Update the state with a new array that includes the saved `Todo`. This re-renders the view.

3. Clear the form input.

Finally, add a method for updating the `todo` state right below `createTodo()`:

`todo-view.ts`

```typescript
updateTodoState(todo: Todo, done: boolean) {
  todo.done = done;
  const updatedTodo = { ...todo }; // (1)
  this.todos = this.todos.map((t) => (t.id === todo.id ? updatedTodo : t)); // (2)
  TodoEndpoint.save(updatedTodo); // (3)
}
```

1. Create a new `Todo` with the updated `done` state.

2. Update the local `todos` array with the new state. The `map` operator creates a new array where the changed `todo` is swapped out. This re-renders the view.

3. Save the updated `todo` to the server.

Get the complete view implementation

Open the `frontend/views/todo/todo-view.ts` file and replace its contents with the following:

`todo-view.ts`

```typescript
import { html } from 'lit';

import { customElement, state } from 'lit/decorators.js';

import '@vaadin/button';
import '@vaadin/checkbox';
import '@vaadin/text-field';
import { Binder, field } from '@vaadin/hilla-lit-form';
import Todo from 'Frontend/generated/com/example/application/Todo';
import TodoModel from 'Frontend/generated/com/example/application/TodoModel';
import { TodoEndpoint } from 'Frontend/generated/endpoints';
import { View } from '../view';

@customElement('todo-view')
export class TodoView extends View {
  @state()
  private todos: Todo[] = [];

  private binder = new Binder(this, TodoModel);

  render() {
    return html`
      <div class="form">
        <vaadin-text-field label="Task" ${field(this.binder.model.task)}></vaadin-text-field>
        <vaadin-button theme="primary" @click=${this.createTodo} ?disabled=${this.binder.invalid}>
          Add
        </vaadin-button>
      </div>
      <div class="todos">
        ${this.todos.map(
          (todo) => html`
            <div class="todo">
              <vaadin-checkbox
                ?checked=${todo.done}
                @checked-changed=${(e: CustomEvent) => this.updateTodoState(todo, e.detail.value)}></vaadin-checkbox>
              <span>${todo.task}</span>
            </div>
        `)}
      </div>
    `;
  }

  async connectedCallback() {
    super.connectedCallback();
    this.todos = await TodoEndpoint.findAll();
  }

  async createTodo() {
    const createdTodo = await this.binder.submitTo(TodoEndpoint.save);
    if (createdTodo) {
      this.todos = [...this.todos, createdTodo];
      this.binder.clear();
    }
  }

  updateTodoState(todo: Todo, done: boolean) {
    todo.done = done;
    const updatedTodo = { ...todo };
    this.todos = this.todos.map((t) => (t.id === todo.id ? updatedTodo : t));
    TodoEndpoint.save(updatedTodo);
  }
}
```

## <a id="run-the-application"></a>6. Run the Application

Run the project from the command line with the following command:

terminal

**Windows**

```bash
mvnw
```

terminal

**macOS / Linux**

```bash
./mvnw
```

The first time you run the application, it may take up to a few minutes, as Hilla downloads all the dependencies and builds a frontend bundle. Subsequent builds don’t download dependencies, so that they are much faster.

When the build has finished, you should see the application running on <http://localhost:8080>.

You should now have a fully functional to-do application. Notice that when you refresh the browser, it keeps the same `todo` items, as they are persisted in the database.

[Image: The to-do application running in a web browser]
