name: writing-extension-devui description: > How to add a Dev UI page to a Quarkus extension: deployment processors, runtime-dev JSON-RPC services, and Lit web components.
Writing a Dev UI for a Quarkus Extension
Dev UI is the interactive dashboard at /q/dev-ui during quarkus:dev.
Extensions add pages via build items, runtime JSON-RPC services, and Lit web
components. See the Dev UI guide for full
documentation.
Directory Layout
my-extension/
deployment/src/main/java/.../deployment/devui/
MyFeatureDevUIProcessor.java # Build steps
deployment/src/main/resources/dev-ui/
qwc-myfeature-dashboard.js # Lit web components
runtime-dev/src/main/java/.../runtime/dev/ui/
MyFeatureJsonRpcService.java # JSON-RPC service
- JS naming:
qwc-<extensionname>-<pagename>.js - JSON-RPC services go in
runtime-dev/, notruntime/. Register as a conditional dev dependency — see theclassloading-and-runtime-devskill.
Deployment Processor
Gate all Dev UI build steps with @BuildStep(onlyIf = IsDevelopment.class) or
use @BuildSteps(onlyIf = IsLocalDevelopment.class) at the class level.
import io.quarkus.devui.spi.page.CardPageBuildItem;
import io.quarkus.devui.spi.page.Page;
@BuildStep(onlyIf = IsDevelopment.class)
CardPageBuildItem devUI() {
CardPageBuildItem card = new CardPageBuildItem();
card.addPage(Page.webComponentPageBuilder()
.title("Dashboard")
.componentLink("qwc-myfeature-dashboard.js")
.icon("font-awesome-solid:robot"));
// Build-time data — available in JS via: import { items } from 'build-time-data';
card.addBuildTimeData("items", someList);
return card;
}
Register the JSON-RPC provider in a separate build step. This does not need the
IsDevelopment guard because the build item is only consumed in dev mode:
import io.quarkus.devui.spi.JsonRPCProvidersBuildItem;
@BuildStep
JsonRPCProvidersBuildItem jsonRpcProvider() {
return new JsonRPCProvidersBuildItem(MyFeatureJsonRpcService.class);
}
Maven dependency for the deployment module:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-devui-spi</artifactId>
</dependency>
Runtime JSON-RPC Service
A plain class in runtime-dev. Every public method becomes a JSON-RPC endpoint
automatically — registration happens via JsonRPCProvidersBuildItem.
public class MyFeatureJsonRpcService {
@Inject
SomeBean bean;
public List<Item> getItems() { return bean.listAll(); }
public boolean doAction(String id) { return bean.execute(id); }
}
- Use
@Injectfor CDI;@PostConstructfor initialization. - Return JSON-serializable data, not HTML.
- For streaming, return
Multi<JsonObject>(Smallrye Mutiny).
Frontend Web Components
Components extend QwcHotReloadElement (not LitElement directly) and use
Vaadin Web Components for consistent styling.
import { QwcHotReloadElement, html, css } from 'qwc-hot-reload-element';
import { JsonRpc } from 'jsonrpc';
import { items } from 'build-time-data';
export class QwcMyfeatureDashboard extends QwcHotReloadElement {
jsonRpc = new JsonRpc(this);
static properties = { _items: { state: true } };
constructor() {
super();
this._items = items;
}
connectedCallback() {
super.connectedCallback();
this.hotReload();
}
hotReload() {
this.jsonRpc.getItems().then(r => { this._items = r.result; });
}
render() {
if (!this._items)
return html`<vaadin-progress-bar indeterminate></vaadin-progress-bar>`;
return html`<vaadin-grid .items="${this._items}" theme="row-stripes">
<vaadin-grid-column path="name" header="Name"></vaadin-grid-column>
</vaadin-grid>`;
}
}
customElements.define('qwc-myfeature-dashboard', QwcMyfeatureDashboard);
build-time-datakeys must match what was passed tocard.addBuildTimeData(key, value).JsonRpcmethod names must match the Java service method names exactly.- Access results via
response.result. - For state updates, use spread:
this._items = [...this._items, newItem]. - Unsubscribe streaming observers in
disconnectedCallback().
Testing
Extend DevUIJsonRPCTest (io.quarkus.devui.tests). Pass the extension
namespace to the super constructor, then call executeJsonRPCMethod():
public class MyFeatureDevUITest extends DevUIJsonRPCTest {
@RegisterExtension
static final QuarkusDevModeTest config = new QuarkusDevModeTest()
.withApplicationRoot((jar) -> jar.addClass(MyBean.class));
public MyFeatureDevUITest() { super("quarkus-myfeature"); }
@Test
public void testGetItems() throws Exception {
JsonNode result = super.executeJsonRPCMethod("getItems");
assertNotNull(result);
}
}
Key Rules
- Correct imports:
CardPageBuildItemis inio.quarkus.devui.spi.page,JsonRPCProvidersBuildItemis inio.quarkus.devui.spi. - JSON-RPC services belong in
runtime-dev/, never inruntime/. - JS files go in
deployment/src/main/resources/dev-ui/. - Extend
QwcHotReloadElement, notLitElement— it provides thehotReload()hook that re-runs on dev-mode restarts. - Return JSON from services, not HTML. Use
Page.externalPageBuilder()for external content like Swagger UI.