A Vaadin Starter Project with a Clear Focus
Many example projects overload the starting point by covering too many topics at once. Routing, data access, security, forms, theme customisations, and other integrations will then be presented in a single demo. This makes it more difficult for readers to recognise the project’s actual structure.
This project takes a different approach. The focus is on a compact framework comprising AppShell, MainLayout, several views, a small service, its own theme, and prepared internationalisation. The aim is to make the basic building blocks of a Vaadin Flow application visible in a way that their interactions are comprehensible.

The source code for this project can be found at
GitHub at: https://3g3.eu/vdn-tpl
This is especially useful in Vaadin Flow, because the interface is created on the server side in Java. Classes such as AppShell, MainLayout, and MainView, therefore, shape the application’s form early on. The article follows a Vaadin-centric perspective and does not primarily examine a specialist domain, but the structure of a usable flow project.
Technology stack and project idea
The project relies on Vaadin Flow 25, Maven, Jetty, JDK 25, classic WAR packaging, its own theme and prepared internationalisation. This combination defines the application’s technical framework.
Vaadin Flow bundles components, layouts, routing, and interactions within a single Java structure. Maven organises build, dependencies, and plugins. Jetty provides a lightweight servlet container for local operation and near-deployment execution. JDK 25 positions the project on a current Java basis, while the WAR packaging underlines its character as a classic web application.
Theme and internationalisation add two cross-cutting aspects to the stack: visual design and language-dependent content. As a result, the technical substructure is not only executable, but already prepared for typical extensions.
Project Structure at a Glance
The application follows the classic Maven structure with src/main/java for source code and src/main/resources for accompanying resources. This structure creates a familiar framework and separates executable code from configuration and language resources.
Within the Java part, responsibilities are structured to match the application structure. Entry point and global configurations form the outer frame. Layout classes define the overarching UI framework, views represent concrete pages, and service classes handle supporting logic.
Resources for internationalisation and the theme complement this. Translation files are located in the resource area, and visual adjustments are bundled in the expected places. This makes it easy to see in the repository where new pages, additional logic or design adjustments are classified.
Application Entry Point: AppShell
The AppShell defines the application’s global layer. This is where settings are anchored that affect not only a single page, but the browser appearance as a whole. These include theme activation, meta information, viewport information or other application-wide configurations.
In the project, this role is implemented in a compact class:
/** * Typical use cases of AppShell * ✅ Viewport & mobile optimization * ✅ Setting metadata (SEO, security) * ✅ Favicons, touch icons * ✅ Global JavaScript snippets (analytics, monitoring) * ✅ Global CSS (e.g., corporate branding) * ✅ Selecting a theme for the entire app */@Meta(name = "author", content = "Sven Ruppert")@Viewport("width=device-width, initial-scale=1.0")@PWA(name = "Project Base for Vaadin", shortName = "Project Base")@Theme("my-theme")@Pushpublic class AppShell implements AppShellConfigurator { @Override public void configurePage(AppShellSettings settings) {// settings.addFavIcon("icon",// "icons/my-favicon.png",// "32x32");//// // Externes CSS// settings.addLink("stylesheet",// "https://cdn.example.com/styles/global.css");//// // Externes Script// settings.addInlineWithContents(// "console.log('Hello from AppShell!');",// Inline.Wrapping.AUTOMATIC); }}
The annotations already show very well what kind of responsibility belongs in this class. @Meta complements global metadata, @Viewport defines behavior on mobile devices, @PWA describes basic progressive web app properties, @Theme(“my-theme”) activates the application-wide theme, and @Push unlocks server-side push communication. In this way, the AppShell bundles only the settings relevant to the browser’s overall appearance.
In addition, the class implements AppShellConfigurator and provides a dedicated extensibility point with configurePage(AppShellSettings settings). Even though the method does not include any active settings in its current state, the commented-out examples already show typical fields of application: Favicons, global stylesheets, or inline scripts can be registered centrally here. This means the project has already been structurally prepared to accommodate such global adaptations.
Your job isn’t to render business content or bundle component logic. Rather, it defines the frame in which the actual surface will later move. This creates a clearly recognisable place for global decisions, rather than scattering them across views or layout classes.
The Basic Structure of the UI with MainLayout
The MainLayout forms the permanent structural level of the surface. It bundles navigation, the header area, and the embedding of the content area, and thus determines how individual pages are presented within the same application.
In the project, this task is implemented in its own layout class, which inherits directly from AppLayout:
public class MainLayout extends AppLayout { public MainLayout() { createHeader(); } private void createHeader() { H1 appTitle = new H1("Vaadin Flow Demo"); SideNav views = getPrimaryNavigation(); Scroller scroller = new Scroller(views); scroller.setClassName(LumoUtility.Padding.SMALL); DrawerToggle toggle = new DrawerToggle(); H2 viewTitle = new H2("Headline"); HorizontalLayout wrapper = new HorizontalLayout(toggle, viewTitle); wrapper.setAlignItems(FlexComponent.Alignment.CENTER); wrapper.setSpacing(false); VerticalLayout viewHeader = new VerticalLayout(wrapper); viewHeader.setPadding(false); viewHeader.setSpacing(false); addToDrawer(appTitle, scroller); addToNavbar(viewHeader); setPrimarySection(Section.DRAWER); } private SideNav getPrimaryNavigation() { SideNav sideNav = new SideNav(); sideNav.addItem(new SideNavItem("Dashboard", "/" + MainView.PATH, DASHBOARD.create()), new SideNavItem("Youtube", "/" + YoutubeView.PATH, CART.create()), new SideNavItem("About", "/" + AboutView.PATH, USER_HEART.create()) ); return sideNav; }}
Recurring elements are centrally housed here rather than rebuilt in each view. On this basis, individual pages can concentrate on their own content, while the MainLayout provides the overarching framework. Vaadin’s AppLayout provides a suitable basis for this.
Between AppShell and views, the MainLayout thus serves as the connecting UI layer: global settings remain outside, concrete content is within the views, and in between lies the permanent interface structure.
Setting up navigation and routes cleanly
The routing structure determines the paths to the individual views. In Vaadin Flow, this is done directly at the respective class via @Route. This keeps routing closely related to the visible page structure.
In the project, this principle is evident in the view classes. The start page uses an empty path and binds to the MainLayout at the same time:
/** * The main view contains a text field for getting the username and a button * that shows a greeting message in a notification. */@Route(value = MainView.PATH, layout = MainLayout.class)public class MainView extends VerticalLayout implements LocaleChangeObserver { public static final String YOUR_NAME = "your.name"; public static final String SAY_HELLO = "say.hello"; public static final String PATH = ""; private final GreetService greetService = new GreetService(); private final Button button = new Button(); private final TextField textField = new TextField(); public MainView() { button.addClickListener(e -> { add(new Paragraph(greetService.greet(textField.getValue()))); }); add(textField, button); } @Override public void localeChange(LocaleChangeEvent localeChangeEvent) { button.setText(getTranslation(SAY_HELLO)); textField.setLabel(getTranslation(YOUR_NAME)); }}
The other pages also follow the same pattern. AboutView and YoutubeView each define their paths via their own constants and are also embedded in the MainLayout:
@Route(value = AboutView.PATH, layout = MainLayout.class)public class AboutView extends VerticalLayout { public static final String PATH = "about"; public AboutView() { H1 title = new H1("About"); H2 subtitle = new H2("Vaadin Flow Demo Application"); Paragraph description = new Paragraph("This is a demo application built with Vaadin Flow " + "framework to showcase various UI components and features."); Paragraph version = new Paragraph("Version: 1.0.0"); Paragraph author = new Paragraph("Created by: Sven Ruppert"); Paragraph bio = new Paragraph(""" Sven Ruppert has been involved in software development for more than 20 years. \ As developer advocate he is constantly looking for innovations in software development. \ He is speaking internationally at conferences and has authored numerous technical articles and books."""); Paragraph homepage = new Paragraph( new Paragraph("Visit my website: "), new Anchor("https://www.svenruppert.com", "www.svenruppert.com", BLANK)); Image vaadinLogo = new Image("images/vaadin-logo.png", "Vaadin Logo"); vaadinLogo.setWidth("200px"); setSpacing(true); setPadding(true); setAlignItems(Alignment.CENTER); add(title, subtitle, description, version, author, bio, homepage, vaadinLogo); }}
These excerpts show several important points. First, routing is defined directly where the respective page is implemented. Secondly, the layout = MainLayout.class assignment makes it clear on which surface frame the view will appear later. Third, the paths are not distributed as magic strings in many places, but bundled via PATH constants at the classes themselves.
Routes do not only take on a technical function. They organise the application into accessible areas and make it visible how pages are named and separated from each other. In conjunction with the MainLayout, this creates the navigation structure that users use to switch between individual views.
It is important to distinguish between routing and navigation: Routing defines accessibility, navigation defines its representation in the interface.
The First View: MainView as a Minimal Interaction Example
With MainView, the application can be experienced as a concrete page on first launch. It shows a simple interaction pattern of input, action, and visible reaction, making the basic idea of server-side UI programming in Vaadin immediately tangible.
In the project, this first view is deliberately kept compact:
/** * The main view contains a text field for getting the username and a button * that shows a greeting message in a notification. */@Route(value = MainView.PATH, layout = MainLayout.class)public class MainView extends VerticalLayout implements LocaleChangeObserver { public static final String YOUR_NAME = "your.name"; public static final String SAY_HELLO = "say.hello"; public static final String PATH = ""; private final GreetService greetService = new GreetService(); private final Button button = new Button(); private final TextField textField = new TextField(); public MainView() { button.addClickListener(e -> { add(new Paragraph(greetService.greet(textField.getValue()))); }); add(textField, button); } @Override public void localeChange(LocaleChangeEvent localeChangeEvent) { button.setText(getTranslation(SAY_HELLO)); textField.setLabel(getTranslation(YOUR_NAME)); }}
This class shows the basic pattern of a Vaadin view very well. The page is directly linked to the routing via @Route and is also embedded in the MainLayout. TextField and Button define two central UI components that are directly assembled in the constructor. Clicking on the button triggers a server-side action and adds a new paragraph with the result to the layout. Interface, event handling and reaction thus remain merged in a closed Java model.
The already integrated multilingualism is also remarkable. Through LocaleChangeObserver, the view reacts to a language change and updates identifiers such as button text and field labels directly using translation keys. In this way, the class combines interaction and internationalisation in a very small but complete example.
The actual text logic is no longer directly in the view, but in a small service class:
public class GreetService implements Serializable { public String greet(String name) { if (name == null || name.isEmpty()) { return "Hello anonymous user"; } else { return "Hello " + name; } }}
It is precisely this reduction that makes it easy to see how a first interactive page in Vaadin is structured. Components, event handling, layout integration, and a small amount of outsourced logic are already intertwined without the need for additional infrastructure. At the same time, the MainView remains part of the larger application framework of Routing and MainLayout.
Separating UI Logic and Business Logic
With the first interactive view, the question of responsibility arises. A view describes the interface, inputs, and reactions to user actions. Supporting logic, on the other hand, can be outsourced to its own classes.
In the project, this role is taken over by the GreetService. Even though its task remains small, it shows the boundary between component control in the view and application-oriented logic outside the interface. This separation facilitates later expansions, because additional rules or preparation steps do not have to be accommodated directly in the view.
Thinking about internationalisation from the outset
Texts are created directly in a Vaadin application, where components are built. This is precisely why it makes sense to remove language-dependent content from views early and manage it through translation keys and resource files.
In the project, this is visible directly in the MainView. The class implements LocaleChangeObserver and obtains labels not as hardwired strings, but via translation keys:
/** * The main view contains a text field for getting the username and a button * that shows a greeting message in a notification. */@Route(value = MainView.PATH, layout = MainLayout.class)public class MainView extends VerticalLayout implements LocaleChangeObserver { public static final String YOUR_NAME = "your.name"; public static final String SAY_HELLO = "say.hello"; public static final String PATH = ""; private final GreetService greetService = new GreetService(); private final Button button = new Button(); private final TextField textField = new TextField(); public MainView() { button.addClickListener(e -> { add(new Paragraph(greetService.greet(textField.getValue()))); }); add(textField, button); } @Override public void localeChange(LocaleChangeEvent localeChangeEvent) { button.setText(getTranslation(SAY_HELLO)); textField.setLabel(getTranslation(YOUR_NAME)); }}
The localeChange(…) method, in particular, demonstrates the practical implementation of multilingualism. Instead of entering text directly when components are created, they are resolved via getTranslation(…) by key. This allows the same view to display different labels depending on the active language.
The associated resources are located in the project at src/main/resources/vaadin-i18n. The default language is defined in translations.properties:
your.name=Your name
say.hello=Say Hello
There is a separate file for German translations_de.properties:
your.name=Your name
say.hello=Say hello
These files clearly show how the language-dependent content is extracted from the Java code. The view only knows the keys your.name and say.hello, while the actual texts are maintained in the resources. This means that the interface remains linguistically adaptable without having to rephrase component classes themselves.
Multilingualism is already prepared in the project. Texts are not treated as fixed components of individual components, but as content that is provided depending on the active language. In conjunction with language-dependent interface updates, it becomes apparent that internationalisation not only comprises files but also describes the application’s behaviour.
Additional views as a blueprint for your own pages
In addition to the MainView, AboutView, and YouTubeView, the application also displays other page types. They stand for more information-oriented content and make it clear that the same structure works not only for interactive input pages, but also for simpler content pages.
This makes it possible to see how new views can be embedded in routing, navigation and MainLayout without introducing their own special structures. Different page types can thus be implemented within a common framework.
Build, Start, and Development Mode
Hands-on work with the project includes build, local startup, and development mode. Maven bundles dependencies, build steps and plugin configurations for this purpose. This is particularly relevant for Vaadin because, in addition to the Java code, themes, resources, and Vaadin-specific processing are integrated into the development process.
Jetty takes over the local operation via a lightweight servlet container. This keeps the path from the source code to the running application short. Changes to views, layouts, translations or theme rules can be checked directly in development mode.
WAR packaging fits into this model and keeps the application in a classic web application context.
Conclusion
The project makes the central layers of a Vaadin Flow application visible in a compact form: global configuration via the AppShell, permanent UI structure in the MainLayout, routing for accessible pages, concrete views for content, an outsourced service for supporting logic, prepared internationalisation, a separate theme and a clear build and start path.
Especially in this compact form, it is easy to understand how these building blocks work together. The repository thus shows not only individual Vaadin techniques, but also the structure of an application whose structure remains consistently recognisable from the first entry point to the development mode.
















