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

# Spring Boot & React Full-Stack Development

Hilla is a full-stack web framework for Java. It can help you build better business applications faster by combining a Spring Boot backend with a React frontend. Plus, it has more than forty UI components that you can utilize for a more professional application.

In this tutorial, you’ll learn the basics of Hilla development by building a full-stack CRM application. The code snippets are optimized so you may *copy and paste* them to have a working application up and running quickly.

You can find the complete source code for this tutorial on [GitHub](https://github.com/vaadin/hilla-crm-tutorial).

[Image: The completed application has a data grid and a form.]

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

To follow along on this tutorial and to build a full-stack CRM application, you’ll need only a few things in place:

- Java 17 or later;

- Node 18 or later;

- An IDE that supports Java and TypeScript — IntelliJ Ultimate and VS Code are good choices; and

- Intermediate Java skills.

## <a id="download-project-starter"></a>Download Project Starter

To begin, you’ll need to download the Project Starter. You can do that by clicking on the button here.

[Download Project Starter (zip)](https://start.vaadin.com/dl?preset=hilla-crm-tutorial\&projectName=hilla-crm)

You could instead use the Hilla CLI to create a new project by entering the following from the command line:

```terminal
npx @hilla/cli init --preset=hilla-crm-tutorial hilla-crm
```

## <a id="project-structure"></a>Project Structure

The Starter is a standard Hilla project with a test database, and repositories for accessing it. Here’s a quick overview of the relevant parts of the project:

- `src/main/frontend` contains the React frontend

  - `views`

    - `@layout.tsx` is the parent layout that includes the navigation bar

    - `contacts`

      - `@index.tsx` is the view we are implementing

- `src/main/java` is the Spring Boot backend

  - `com.example.application`

    - `Application.java` is the main Spring Boot application class

    - `data` contains all the database-access-related code

      - `Contact.java` and `Company.java` are the JPA entities

      - `ContactRepository.java` and `CompanyRepository.java` are repositories for accessing the database

    - `services` is where we will create a backend service for the application

- `main/resources/data.sql` is an SQL script that populates the database with demo data on each start

## <a id="run-the-hilla-application"></a>Run the Hilla Application

There are two ways to run an application: you can use your IDE to run `Application.java` — this is recommended since it supports debugging; or you can run the default Maven goal from the command-line (i.e., `mvn`).

Hilla will automatically reload changes to frontend and backend code when saving. IntelliJ requires you to build manually the project after making changes to Java code, while VS Code will build automatically the project on save.

## <a id="create-a-browser-callable-java-service"></a>Create a Browser-Callable Java Service

Hilla makes it possible to call Java classes from TypeScript with the `@BrowserCallable` annotation. To do this, create a `CRMService.java` class in the `com.example.application.services` package. The service handles both listing and saving Contacts. Add the following code to the class:

`CRMService.java`

```java
package com.example.application.services;

import com.example.application.data.Company;
import com.example.application.data.CompanyRepository;
import com.example.application.data.Contact;
import com.example.application.data.ContactRepository;
import com.vaadin.flow.server.auth.AnonymousAllowed;
import com.vaadin.hilla.BrowserCallable;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;

import java.util.List;


@AnonymousAllowed
@BrowserCallable
public class CRMService {

    private final CompanyRepository companyRepository;
    private final ContactRepository contactRepository;

    public CRMService(CompanyRepository companyRepository, ContactRepository contactRepository) {
        this.companyRepository = companyRepository;
        this.contactRepository = contactRepository;
    }

    public record ContactRecord(
            Long id,
            @NotNull
            @Size(min = 1, max = 50)
            String firstName,
            @NotNull
            @Size(min = 1, max = 50)
            String lastName,
            @NotNull
            @Email
            String email,
            @NotNull
            CompanyRecord company
    ) {
    }

    public record CompanyRecord(
            @NotNull
            Long id,
            String name
    ) {
    }


    private ContactRecord toContactRecord(Contact c) {
        return new ContactRecord(
                c.getId(),
                c.getFirstName(),
                c.getLastName(),
                c.getEmail(),
                new CompanyRecord(
                        c.getCompany().getId(),
                        c.getCompany().getName()
                )
        );
    }

    private CompanyRecord toCompanyRecord(Company c) {
        return new CompanyRecord(
                c.getId(),
                c.getName()
        );
    }

    public List<CompanyRecord> findAllCompanies() {
        return companyRepository.findAll().stream()
                .map(this::toCompanyRecord).toList();
    }

    public List<ContactRecord> findAllContacts() {
        List<Contact> all = contactRepository.findAllWithCompany();
        return all.stream()
                .map(this::toContactRecord).toList();
    }

    public ContactRecord save(ContactRecord contact) {
        var dbContact = contactRepository.findById(contact.id).orElseThrow();
        var company = companyRepository.findById(contact.company.id).orElseThrow();

        dbContact.setFirstName(contact.firstName);
        dbContact.setLastName(contact.lastName);
        dbContact.setEmail(contact.email);
        dbContact.setCompany(company);

        var saved = contactRepository.save(dbContact);

        return toContactRecord(saved);
    }

}
```

- The `@BrowserCallable` annotation makes all public methods in the service available to call from TypeScript.

- `@AnonymousAllowed` turns off access control for this service. Check out the security section to learn how Hilla uses Spring Security to secure server access.

- The service injects `ContactRepository` and `CompanyRepository` in the constructor for database access.

- This defines DTOs for the view as Java Records, including validation annotations that you want to enforce, both in the UI and the service.

- The service defines the CRUD methods needed for the CRM.

Now, you’ll have to build the application. Hilla will generate the needed TypeScript for accessing the service.

## <a id="listing-contacts-in-a-data-grid"></a>Listing Contacts in a Data Grid

With the backend completed, you can start building the UI. Change the contents of `Frontend/views/contacts/ContactsView.tsx` to the following:

@index.tsx

```tsx
import ContactRecord from 'Frontend/generated/com/example/application/services/CRMService/ContactRecord';
import { useEffect, useState } from 'react';
import { CRMService } from "Frontend/generated/endpoints";
import { Grid, GridColumn } from "@vaadin/react-components";
import ContactForm from "Frontend/views/contacts/ContactForm";
import { ViewConfig } from "@vaadin/hilla-file-router/types.js";

export const config: ViewConfig = {
    menu: {
        title: 'Contacts',
    },
};

export default function ContactsView() {
    const contacts = useSignal<ContactRecord[]>([]);
    const selected = useSignal<ContactRecord | null | undefined>(undefined);

    useEffect(() => {
        CRMService.findAllContacts().then(found => contacts.value = found);
    }, []);

    return (
        <div className="p-m flex gap-m">
            <Grid
                items={contacts.value}
                onActiveItemChanged={e => selected.value = e.detail}
                selectedItems={[selected.value]}>

                <GridColumn path="firstName"/>
                <GridColumn path="lastName"/>
                <GridColumn path="email" autoWidth/>
                <GridColumn path="company.name" header="Company name"/>
            </Grid>
        </div>
    );
}
```

- This calls `CRMService.findAllContacts` in a React `useEffect`. It ensures the call only happens once by passing an empty dependency array. When the async call finishes, the contacts are updated into the contacts state.

- The contacts are bound to a `<Grid>` component that defines columns for each property you want to display in the grid.

- The selected grid row is stored in the selected state variable. In the next step, you’ll bind the selected contact to a form for editing.

Reload your browser, and you should now see a data grid displaying all of the contacts created using the example data of `main/resources/data.sql`.

## <a id="create-a-form-for-editing-contacts"></a>Create a Form for Editing Contacts

For a complete CRM, users need to be able to edit contacts. Create a new component `ContactForm.tsx` in `frontend/views/contacts`:

ContactForm.tsx

```tsx
import { TextField, EmailField, Select, SelectItem, Button} from "@vaadin/react-components";
import { useForm } from "@vaadin/hilla-react-form";
import ContactRecordModel from "Frontend/generated/com/example/application/services/CRMService/ContactRecordModel";
import { CRMService } from "Frontend/generated/endpoints";
import { useEffect, useState } from "react";
import ContactRecord from "Frontend/generated/com/example/application/services/CRMService/ContactRecord";
import {ViewConfig} from "@vaadin/hilla-file-router/types.js";

export const config: ViewConfig = {
    menu: {
        exclude: true
    },
};

interface ContactFormProps {
    contact?: ContactRecord | null;
    onSubmit?: (contact: ContactRecord) => Promise<void>;
}

export default function ContactForm({contact, onSubmit}: ContactFormProps) {

    const companies = useSignal<SelectItem[]>([]);

    const {field, model, submit, reset, read} = useForm(ContactRecordModel, { onSubmit } );

    useEffect(() => {
        read(contact);
    }, [contact]);

    useEffect(() => {
        getCompanies();
    }, []);

    async function getCompanies() {
        const companies = await CRMService.findAllCompanies();
        const companyItems = companies.map(company => {
            return {
                label: company.name,
                value: company.id + ''
            };
        });
        companies.value = companyItems;
    }

    return (
        <div className="flex flex-col gap-s items-start">

            <TextField label="First name" {...field(model.firstName)} />
            <TextField label="Last name" {...field(model.lastName)} />
            <EmailField label="Email" {...field(model.email)} />
            <Select label="Company" items={companies.value} {...field(model.company.id)} />

            <div className="flex gap-m">
                <Button onClick={submit} theme="primary">Save</Button>
                <Button onClick={reset}>Reset</Button>
            </div>
        </div>
    )
}
```

- The form component takes in a contact and `onSubmit` callback method as properties.

- The Hilla `useForm` hook uses the automatically generated `ContactRecordModel` to configure a form based on the validation rules you defined in Java.

- The UI fields are bound to the form with `{…​field(model.property)}`. Hilla will manage the form value and validations.

- Use an effect to read the passed-in contact into the form any time it changes.

- Use an effect to fetch all companies from `CRMService` and convert them to objects with label-value pairs for the select component.

Change `ContactsView.tsx` with the following content:

ContactsView\.tsx

```tsx
import ContactRecord from 'Frontend/generated/com/example/application/services/CRMService/ContactRecord';
import { useEffect } from 'react';
import { useSignal } from '@vaadin/hilla-react-signals';
import { CRMService } from 'Frontend/generated/endpoints';
import { Grid } from '@vaadin/react-components/Grid';
import { GridColumn } from '@vaadin/react-components/GridColumn';
import ContactForm from 'Frontend/views/contacts/ContactForm';

export default function ContactsView() {
    const contacts = useSignal<ContactRecord[]>([]);
    const selected = useSignal<ContactRecord | null | undefined>(undefined);

    useEffect(() => {
        CRMService.findAllContacts().then(found => contacts.value = found);
    }, []);

    async function onContactSaved(contact: ContactRecord) {
        const saved = await CRMService.save(contact)
        if (contact.id) {
            contacts.value = contacts.value.map(current => current.id === saved.id ? saved : current);
        } else {
            contacts.value = [...contacts.value, saved];
        }
        selected.value = saved;
    }

    return (
        <div className="p-m flex gap-m">
            <Grid
                items={contacts.value}
                onActiveItemChanged={e => setSelected(e.detail.value)}
                selectedItems={[selected.value]}>

                <GridColumn path="firstName"/>
                <GridColumn path="lastName"/>
                <GridColumn path="email"/>
                <GridColumn path="company.name" header="Company name"/>
            </Grid>

            {selected.value &&
                <ContactForm contact={selected.value} onSubmit={onContactSaved}/>
            }
        </div>
    );
}
```

- The form is conditionally rendered if there is a selected item.

- On submission, the updated contact is saved to `CRMService`.

- If the saved contact had an id (i.e., an existing contact), update the contacts state by swapping the updated contact.

- If the contact is new, create a new contacts array and append the new contact.

- Finally, select the newly saved item.

Refresh your browser, and try the application. You should now have a fully functional, full-stack application for listing and editing contacts. Verify that the changes are persisted in the database by refreshing your browser after making a change.

## <a id="build-for-production"></a>Build for Production

If you want to share your application with others, you’ll need to create a production build. It’ll generate an optimized build and turn off development-time debugging.

> **Note:** Your application has an in-memory database populated with demo data on each start. Remove the data initializer and change the database to a persistent database like PostgreSQL, MySQL, MariaDB, or something similar for a real production application.

Create a production-ready JAR in the target folder with the following Maven command:

```terminal
mvn package -Pproduction
```

The resulting JAR file is a standard Spring Boot application that you can run or deploy anywhere Java applications are supported.

Alternatively, you can use Spring Boot’s built-in [buildpacks](https://buildpacks.io/) support to create a Docker image:

```terminal
mvn spring-boot:build-image -Pproduction
```

Hilla also supports [compiling GraalVM native images](https://vaadin.com/docs/v24/hilla/guides/production/native.md) to optimize further the startup time or the memory consumption.

You can find the complete source code for this tutorial on [GitHub](https://github.com/vaadin/hilla-crm-tutorial).

`c48fcda0-21e9-4c27-b639-6fa5fe21bb7d`
