Skip to content
JDesk
Guides

Choosing a frontend

Choose between plain HTML, React, Vue, and Svelte for a JDesk user interface.

Pick the frontend stack for a new JDesk app and understand what each template gives you. Every template ships the same Java core and the same bridge contract; they differ only in how the ui/ folder is written and built. This guide assumes you have scaffolded before (see Scaffolding).

Pick a template when you scaffold#

create-jdesk-app selects the frontend through the --template flag or the interactive menu:

npx create-jdesk-app@latest my-app --template react --package com.acme.myapp

Run it with no project name for the menu:

npx create-jdesk-app@latest
Select a template:
  1) Basic           — single Gradle module, plain HTML/JS frontend (great for learning)
  2) Vanilla + Vite  — single module, Vite + vanilla TypeScript
  3) React + Vite    — single module, Vite + React
  4) Vue + Vite      — single module, Vite + Vue
  5) Svelte + Vite   — single module, Vite + Svelte
  6) Structured      — multi-module: domain / application / infrastructure / desktop

The valid --template values are basic, vanilla, react, vue, svelte, and structured (default basic). An unknown name fails before any files are written.

What each template gives you#

TemplateFrontendDev serverProduction buildProject shape
basicPlain HTML/JS, no frameworkViteBuild.java (copies files)Single Gradle module
vanillaVite + vanilla JS/TSVitevite buildSingle module
reactVite + React 19Vitevite buildSingle module
vueVite + Vue 3Vitevite buildSingle module
svelteVite + Svelte 5Vitevite buildSingle module
structuredPlain HTML/JS, no frameworkViteBuild.java (copies files)Four modules: domain, application, infrastructure, desktop

Two axes decide the choice:

  • Frontend stack. basic and structured ship a dependency-free ui/ — plain index.html, src/main.js, src/style.css. vanilla, react, vue, and svelte add the corresponding framework dependencies and let Vite bundle them.
  • Project shape. structured is the only multi-module template. It splits Java across domain, application, infrastructure, and desktop modules and runs the plugin from the desktop module. Use it when you want a layered architecture; use any of the others for a single application module.

structured uses the same plain-HTML frontend as basic, so pick it for its Java layout, not its UI stack. If you want a framework and a layered backend, start from structured and add a framework to ui/ yourself.

The bridge contract every frontend uses#

Whatever template you choose, the frontend talks to Java over the same injected channel — the bridge, reachable as window.__jdesk. The contract is identical across all templates:

  1. On each navigation, the platform layer injects a fresh nonce as a control envelope delivered on the document:

    document.addEventListener("jdesk-message", ({ detail }) => {
      const message = JSON.parse(detail);   // detail is a JSON string
      // { "v": 1, "kind": "nonce", "nonce": "<hex>" }
    });
    
  2. The frontend echoes the nonce back in a hello and waits for helloAck:

    window.__jdesk.post(JSON.stringify({
      v: 1, kind: "hello", client: "my-app", clientVersion: "0.1.0", nonce
    }));
    
  3. After the handshake, the frontend sends invoke envelopes and correlates each result by id:

    window.__jdesk.post(JSON.stringify({
      v: 1, kind: "invoke", id: "request-1", command: "greeting.greet", nonce,
      payload: { name: "JDesk" }
    }));
    

Outgoing messages always go through window.__jdesk.post(json); incoming messages always arrive as jdesk-message CustomEvents whose detail is the JSON envelope string. The generated src/main.js in each template is a working reference for this flow. See the IPC protocol for every envelope shape and the processing order.

How ui/ builds and where dist goes#

The Gradle plugin's frontend { } block wires the same three commands in every template:

PropertyValue
devCommandnpm run devvite --host 127.0.0.1 --port 5173 --strictPort
devUrlhttp://127.0.0.1:5173
distDirectoryui/dist

The buildCommand is where the templates diverge:

  • basic and structured run a single-file Java program with the JDK's source launcher:

    buildCommand.set(javaLauncher.map {
        listOf(it.executablePath.asFile.absolutePath, "Build.java")
    })
    

    ui/Build.java creates dist/ and copies index.html, src/main.js, and src/style.css into it (rewriting the /src/main.js script path to ./main.js). There is no bundler in the production build — the plain files are copied verbatim.

  • vanilla, react, vue, svelte run Vite:

    buildCommand.set(listOf("npm", "run", "build"))   // vite build
    

Either way, jdeskFrontendBuild runs the buildCommand in ui/ and writes the result to distDirectory (ui/dist). That built directory is then packed into the application jar under /web by processResources, so jdesk://app/index.html resolves to your built assets in the packaged app. See The dev loop and the Gradle plugin reference.

When to use the generated TypeScript client#

Every template's src/main.js speaks the raw bridge protocol directly. That keeps the templates dependency-free, but you write the nonce/hello/invoke plumbing by hand. For anything beyond a first command, switch to the generated typed client:

  • Raw bridge — good for learning the protocol and for the smallest possible frontend. You build envelopes and parse jdesk-message events yourself.

  • Generated clientjdesk-codegen emits types.ts and commands.ts at compile time, and jdesk-client provides the invoke runtime they call. You then write:

    import { commands } from "./generated/commands";
    
    const response = await commands.greeting.greet({ name: "Ada" });
    

    The handshake, request ids, timeouts, and cancellation are handled for you, and the call is fully typed against your Java records.

See Generate TypeScript bindings for how to wire the generated client into any template.

Next steps#