In this guide, we create a web UI that performs full CRUD (create, read, update, and delete) operations on a Spring Data JPA backend. This UI is developed entirely in pure Java using Vaadin Flow (no HTML or JavaScript involved). You can explore the full source code on GitHub.
What You Need
- About 15 minutes
- JDK 17 or later
Step 1: Import a Starter Project
Click here to download an empty project starter. Unpack the downloaded zip into a folder on your computer, and import the project in the IDE of your choice.
The pom.xml
of the starter project comes with all the dependencies necessary to complete this tutorial.
Step 2: Create the Backend Classes
Let's start by creating a simple JPA entity and a Spring Data repository.
The following Person
class is a simple JPA entity with three fields, representing a person's first name, last name, and email.
package com.example.application; import jakarta.persistence.Entity; import jakarta.persistence.Id; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; @Entity public class Person { @NotBlank private String firstName; @NotBlank private String lastName; @Id @NotBlank @Email private String email; public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } }
We also create a repository interface using Spring Data in order to work with Person
entities as follows:
package com.example.application; import org.springframework.data.jpa.repository.JpaRepository; public interface PersonRepository extends JpaRepository<Person, Long> { }
In order to work with some data, we also populate the embedded H2 database with some random entries that are generated using the ExampleDataGenerator
helper class. The following DataGenerator
class generates 100 random entities.
package com.example.application; import com.vaadin.exampledata.DataType; import com.vaadin.exampledata.ExampleDataGenerator; import com.vaadin.flow.spring.annotation.SpringComponent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; import org.springframework.context.annotation.Bean; import java.time.LocalDateTime; @SpringComponent public class DataGenerator { @Bean public CommandLineRunner loadData(@Autowired PersonRepository personRepo) { return args -> { Logger logger = LoggerFactory.getLogger(getClass()); logger.info("Generating demo data"); logger.info("... generating 100 Sample Person entities..."); ExampleDataGenerator<Person> samplePersonGenerator = new ExampleDataGenerator<>( Person.class, LocalDateTime.now()); samplePersonGenerator.setData(Person::setFirstName, DataType.FIRST_NAME); samplePersonGenerator.setData(Person::setLastName, DataType.LAST_NAME); samplePersonGenerator.setData(Person::setEmail, DataType.EMAIL); personRepo.saveAll(samplePersonGenerator.create(100, 123)); logger.info("Generated demo data"); }; } }
Step 3: Create the Editor Layout
Next, we create the Editor Layout, which is the part of the UI that will enable users to manipulate the currently selected Person
entity. We can do this in pure Java using Vaadin, which comes with an extensive set of UI components that we can use as the building blocks for any application.
The following EditorLayout
class extends Vaadin VerticalLayout
, and it includes three TextField
components to hold the first name, last name, and email of the selected Person
entity. EditorLayout
also includes three Button
components that will later be used to perform the CRUD operations. (Note, the save button is used to both create and update entities).
package com.example.application; import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.ButtonVariant; import com.vaadin.flow.component.formlayout.FormLayout; import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.component.textfield.TextField; public class EditorLayout extends VerticalLayout { private TextField firstName; private TextField lastName; private TextField email; private Button deleteButton = new Button("Delete..."); private Button cancelButton = new Button("Cancel"); private Button saveButton = new Button("Save"); public EditorLayout() { FormLayout formLayout = new FormLayout(); firstName = new TextField("First Name"); lastName = new TextField("Last Name"); email = new TextField("Email"); formLayout.add(firstName, lastName, email); HorizontalLayout buttonLayout = new HorizontalLayout(); deleteButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY, ButtonVariant.LUMO_ERROR); cancelButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); buttonLayout.setWidthFull(); buttonLayout.setJustifyContentMode(JustifyContentMode.BETWEEN); buttonLayout.setSpacing(false); buttonLayout.add(deleteButton, new HorizontalLayout(cancelButton, saveButton)); add(formLayout, buttonLayout); setWidth("600px"); setMinWidth("300px"); setFlexGrow(0); } public Button getDeleteButton() { return deleteButton; } public Button getCancelButton() { return cancelButton; } public Button getSaveButton() { return saveButton; } }
Step 4: Create the CRUD View
Now we define the main view that holds both the tabular data as well as the EditorLayout
that was just created. This view extends Vaadin's SplitLayout
component. This SplitLayout shows the tabular data inside a Vaadin Grid
component on the left side of the screen, and the EditorLayout
on its right side. When an item is selected from the Grid, the form is automatically updated.
Note that this view makes use of BeanValidationBinder
in order to bind and validate the values of the EditorLayout
form. Note also that the view is made accessible to the end user via the @Route
annotation (in this case, it would be accessible via the empty `` route).
package com.example.application; import com.vaadin.flow.component.grid.Grid; import com.vaadin.flow.component.notification.Notification; import com.vaadin.flow.component.splitlayout.SplitLayout; import com.vaadin.flow.data.binder.BeanValidationBinder; import com.vaadin.flow.data.binder.ValidationException; import com.vaadin.flow.data.provider.DataProvider; import com.vaadin.flow.data.provider.ListDataProvider; import com.vaadin.flow.router.Route; import org.springframework.beans.factory.annotation.Autowired; @Route("") public class CrudView extends SplitLayout { private Grid<Person> grid; private final EditorLayout editorLayout; /** * The currently edited person */ private Person person; ListDataProvider<Person> dataProvider; private final PersonRepository repo; private BeanValidationBinder<Person> binder; public CrudView(@Autowired PersonRepository repo) { this.repo = repo; configureGrid(); editorLayout = new EditorLayout(); configureBinding(); setSizeFull(); addToPrimary(grid); addToSecondary(editorLayout); } private void configureGrid() { // Auto create Grid's columns based on Person.class members grid = new Grid<>(Person.class, true); grid.setSizeFull(); dataProvider = DataProvider.ofCollection(repo.findAll()); grid.setDataProvider(dataProvider); // when a row is selected or deselected, populate form grid.asSingleSelect().addValueChangeListener(event -> { Person person = event.getValue(); if (person != null) { populateForm(person); } else { clearForm(); } }); } private void configureBinding() { binder = new BeanValidationBinder<>(Person.class); //Bind member fields found in the EditorLayout object. binder.bindInstanceFields(editorLayout); editorLayout.getDeleteButton().addClickListener(e -> { if (this.person != null) { repo.delete(this.person); dataProvider.getItems().remove(person); clearForm(); refreshGrid(); Notification.show("Person deleted."); } }); editorLayout.getCancelButton().addClickListener(e -> { clearForm(); refreshGrid(); }); editorLayout.getSaveButton().addClickListener(e -> { try { if (this.person == null) { this.person = new Person(); } binder.writeBean(this.person); repo.save(this.person); dataProvider.getItems().add(person); clearForm(); refreshGrid(); Notification.show("Person details stored."); } catch (ValidationException validationException) { Notification.show("Please enter a valid person details."); } }); } void clearForm() { populateForm(null); } void populateForm(Person person) { this.person = person; binder.readBean(this.person); } public void refreshGrid() { grid.select(null); dataProvider.refreshAll(); } }
Step 5: Run the Application
To run the project from the command line, type mvnw spring-boot:run
(on Windows), or ./mvnw spring-boot:run
(on macOS or Linux).
Then, in your browser, open http://localhost:8080/.
Go further
Congratulations! You have created a web UI that performs full CRUD (create, read, update, and delete) operations on a Spring Data JPA backend. And you did it in pure Java – without the need for any HTML or JavaScript, and without having to expose REST services or think about HTTP requests, responses, and filter chains.
You can explore the full source code on GitHub.
Visit start.vaadin.com to customize your own Vaadin app starter, or learn more about Vaadin Flow in its documentation.
Great article! Thumbs up!