Build a full backoffice in 10 minutes
This is the fastest way to understand what Mateu is really for.
In a few minutes, you can go from a Spring Boot project to a working backoffice with:
- forms
- validation
- actions
- navigation
- relationships
- browser feedback
And you do it all in Java.
What you’ll build
A small admin app with:
- a home screen
- a menu
- a CRUD screen
- validation
- a foreign key relationship
- a success notification
No frontend project. No REST controllers for each screen. No duplicated models.
Step 1 — Create the UI root
Start with a screen:
@UI("/admin")
@Title("Admin")
public class AdminHome {
@Menu
AdminMenu admin;
}
public class AdminMenu {
@Menu
RolesCrudOrchestrator roles;
}
This already gives you:
- a UI root
- a navigation entry
- a place to expose your backoffice modules
Step 2 — Create a real form
Now define the form for one entity.
public class RoleViewModel implements Identifiable, CrudEditorForm<String>, CrudCreationForm<String> {
@EditableOnlyWhenCreating
@NotEmpty
String id;
@NotEmpty
String name;
@Colspan(2)
@Style("width: 100%;")
String description;
@ForeignKey(search = PermissionIdOptionsSupplier.class, label = PermissionIdLabelSupplier.class)
@Colspan(2)
@Style("width: 100%;")
@Stereotype(FieldStereotype.checkbox)
List<String> permissions;
@Override
public String id() {
return id;
}
@Override
public String create(HttpRequest httpRequest) {
return id;
}
@Override
public void save(HttpRequest httpRequest) {
}
}
In one class, you just defined:
- state
- validation
- layout hints
- rendering intent
- a relationship
- create/save lifecycle
Step 3 — Connect it to your application layer
Now wire the form to your actual use cases.
public class RoleViewModel implements Identifiable, CrudEditorForm<String>, CrudCreationForm<String> {
@EditableOnlyWhenCreating
@NotEmpty
String id;
@NotEmpty
String name;
@Colspan(2)
@Style("width: 100%;")
String description;
@ForeignKey(search = PermissionIdOptionsSupplier.class, label = PermissionIdLabelSupplier.class)
@Colspan(2)
@Style("width: 100%;")
@Stereotype(FieldStereotype.checkbox)
List<String> permissions;
final CreateRoleUseCase createRoleUseCase;
final SaveRoleUseCase saveRoleUseCase;
public RoleViewModel(CreateRoleUseCase createRoleUseCase, SaveRoleUseCase saveRoleUseCase) {
this.createRoleUseCase = createRoleUseCase;
this.saveRoleUseCase = saveRoleUseCase;
}
@Override
public String create(HttpRequest httpRequest) {
createRoleUseCase.handle(new CreateRoleCommand(id, name, description, permissions));
return id;
}
@Override
public void save(HttpRequest httpRequest) {
saveRoleUseCase.handle(new SaveRoleCommand(id, name, description, permissions));
}
@Override
public String id() {
return id;
}
}
Mateu does not replace your application architecture.
It sits on top of it.
Step 4 — Add the CRUD adapter
The adapter connects Mateu’s CRUD lifecycle to your query services and use cases.
public class RoleCrudAdapter implements CrudAdapter<
RoleViewModel,
RoleViewModel,
RoleViewModel,
NoFilters,
RoleRow,
String> {
final RoleViewModel viewModel;
final RoleQueryService queryService;
final DeleteRoleUseCase deleteRoleUseCase;
public RoleCrudAdapter(
RoleViewModel viewModel,
RoleQueryService queryService,
DeleteRoleUseCase deleteRoleUseCase) {
this.viewModel = viewModel;
this.queryService = queryService;
this.deleteRoleUseCase = deleteRoleUseCase;
}
@Override
public ListingData<RoleRow> search(String searchText, NoFilters filters, Pageable pageable) {
return queryService.findAll(searchText, filters, pageable);
}
@Override
public void deleteAllById(List<String> selectedIds) {
deleteRoleUseCase.handle(new DeleteRoleCommand(selectedIds));
}
@Override
public RoleViewModel getView(String id) {
return viewModel.load(queryService.getById(id).orElseThrow());
}
@Override
public RoleViewModel getEditor(String id) {
return viewModel.load(queryService.getById(id).orElseThrow());
}
@Override
public RoleViewModel getCreationForm(HttpRequest httpRequest) {
return viewModel;
}
}
Step 5 — Expose the CRUD
Now expose it through a CrudOrchestrator.
@Title("Roles")
public class RolesCrudOrchestrator extends CrudOrchestrator<
RoleViewModel,
RoleViewModel,
RoleViewModel,
NoFilters,
RoleRow,
String> {
final RoleCrudAdapter adapter;
public RolesCrudOrchestrator(RoleCrudAdapter adapter) {
this.adapter = adapter;
}
@Override
public CrudAdapter<RoleViewModel, RoleViewModel, RoleViewModel, NoFilters, RoleRow, String> adapter() {
return adapter;
}
@Override
public String toId(String s) {
return s;
}
}
Now you have a real CRUD screen in your backoffice.
Step 6 — Resolve relationships dynamically
Mateu can resolve foreign keys dynamically through backend suppliers.
Options supplier
public class PermissionIdOptionsSupplier implements ForeignKeyOptionsSupplier {
final PermissionQueryService queryService;
public PermissionIdOptionsSupplier(PermissionQueryService queryService) {
this.queryService = queryService;
}
@Override
public ListingData<Option> search(String searchText, Pageable pageable, HttpRequest httpRequest) {
var found = queryService.findAll(searchText, null, pageable);
return new ListingData<>(new Page<>(
searchText,
found.page().pageSize(),
found.page().pageNumber(),
found.page().totalElements(),
found.page().content().stream()
.map(permission -> new Option(permission.id(), permission.name()))
.toList()
));
}
}
Label supplier
public class PermissionIdLabelSupplier implements LabelSupplier {
final PermissionQueryService queryService;
public PermissionIdLabelSupplier(PermissionQueryService queryService) {
this.queryService = queryService;
}
@Override
public String label(Object id, HttpRequest httpRequest) {
return queryService.getLabel((String) id);
}
}
So your form can work with real relationships without moving logic into the frontend.
Step 7 — Return user feedback directly
You can return browser feedback straight from backend methods.
@Button
public Message notifySave() {
return new Message("Role saved successfully");
}
Mateu turns that into a browser notification.
No frontend toast code required.
What you just built
With a small amount of Java, you defined:
- navigation
- CRUD lifecycle
- validation
- relationships
- rendering behavior
- user feedback
Mateu handled:
- rendering
- browser validation
- interaction
- state binding
- UI updates
Why this matters
In a traditional stack, this usually means:
- backend model
- frontend model
- API glue
- duplicated validation
- duplicated relationships
- duplicated navigation
With Mateu, it stays in one place.
Where to go next
If you want the step-by-step version, continue with:
- Your first Mateu app with Spring Boot →
- Build a real CRUD with relationships →
- Compose a shell with multiple services →
One sentence summary
This is what Mateu is for: building real backoffice applications with minimal code and without a frontend layer.