Quarkus - Quinoa

Quinoa is a Quarkus extension which eases the development, the build and serving single page apps or web components (built with NodeJS: React, Angular, Vue, Lit, …) alongside other Quarkus services (REST, GraphQL, Security, Events, …​).

Live code the backend and frontend together with close to no configuration. When enabled in development mode, Quinoa will start the UI live coding server provided by the target framework and forward relevant requests to it. In production mode, Quinoa will run the build and process the generated files to serve them at runtime.

How it works

The Quinoa build (using npm)

Quinoa Build
packages are installed by Quinoa before the build when needed (i.e npm install). See Packages installation. Quinoa is pre-configured to work with your favorite package manager (npm, yarn or pnpm).

Runtime for production mode

When running jar or binary in production mode:

Quinoa Runtime Production

Runtime for full Quarkus live-coding

Quinoa (using Quarkus live-coding watch feature) will watch the Web UI directory and trigger a new build on changes. It works the same as the production mode. This option is perfect for small/fast builds.

You can differentiate the build for dev mode. e.g to disable minification.

Runtime for proxied live-coding

When running dev-mode (e.g with npm on port 3000):

Quinoa Proxy Dev
Quarkus live-coding will keep watching for the backend changes as usual.


  • Install NodeJS (https://nodejs.org/)

  • Create or use an existing Quarkus application

  • Add the Quinoa extension


Create a new Quinoa project (with a base Quinoa starter code):

quarkus create app quinoa-app -x=io.quarkiverse.quinoa:quarkus-quinoa

Then start the live-coding:

quarkus dev

You could also just add the extension (but you won’t get the starter code):

quarkus ext add io.quarkiverse.quinoa:quarkus-quinoa
  • In your pom.xml file, add this dependency:


Getting Started

Create a Web UI directory in src/main/webui. This directory will contain your NodeJS Web application code with a package.json. The location is configurable, the directory could be outside the Quarkus project as soon as the file are available at build time.


From here, copy your existing Web UI or generate an application from any existing Node based Web UI framework such as React, Angular, Lit, Webpack, Rollup, …​ or your own. Example:

The key points are the package.json scripts (build and optionally test) and the directory where the web files (index.html, scripts, …​) are generated (by default, it will use build/ relative to the ui-dir).

Quinoa provides two options for live-coding:

  • Delegate to the UI live-coding dev server (proxy mode). To enable it, configure the port of the UI server. By convention Quinoa will call the start script from the package.json to start the UI server process. Then it will transparently proxy relevant requests to the given port.

  • Quarkus watches the files and Quinoa triggers a new Web UI build on changes (you can configure different builds for dev and prod).

Start the Quarkus live coding:

$ quarkus dev

It’s done! The web application is now built alongside Quarkus, dev-mode is available, and the generated files will be automatically copied to the right place and be served by Quinoa if you hit http://localhost:8080

2022-03-28 09:24:46,739 INFO  [io.qua.qui.dep.QuinoaProcessor] (build-25) Quinoa target directory: 'xxx/target/quinoa-build'
2022-03-28 09:24:46,739 INFO  [io.qua.qui.dep.QuinoaProcessor] (build-25) Quinoa generated resource: '/favicon.ico'
2022-03-28 09:24:46,740 INFO  [io.qua.qui.dep.QuinoaProcessor] (build-25) Quinoa generated resource: '/index.html'
2022-03-28 09:24:46,741 INFO  [io.qua.qui.dep.QuinoaProcessor] (build-25) Quinoa generated resource: '/simple-greeting.js'
With Quinoa, you don’t need to manually copy the files to META-INF/resources. Quinoa has its own system and will provide another Vert.x route for it. If you have conflicting files with META-INF/resources, Quinoa will have priority over them.

How to use the extension.

Configure the build

Add a build script in the package.json to generate your web application index.html, scripts and assets (styles, images, …​) in some build directory (configurable [quarkus-quinoa_quarkus.quinoa.build-dir].

 "scripts": {
    "start": "[start the Web UI live coding server]",
    "build": "[build the Web UI]",
    "test": "[test the Web UI]"
The build directory will automatically be moved by Quinoa to target/quinoa-build when using Maven (build/quinoa-build with Gradle) in order to be served.

You can differentiate dev from production builds using the MODE environment variable (dev, prod, test):

 "scripts": {
        "build": "if [[ $MODE == \"dev\" ]]; then webpack --mode=development; else webpack --mode=production; fi",

Package manager

It is not necessary (but possible) to configure the package manager (NPM, Yarn or PNPM), it will be auto-detected depending on the project lockfile, NPM is the fallback:

  • Use quarkus.quinoa.package-manager if present

  • Else if yarn.lock then Yarn

  • Else if pnpm-lock.yaml then PNPM

  • Else NPM

Quinoa is configured with the commands to call depending on the chosen package manager (to always keep the same behavior and make it easy to switch).

Node packages installation (node_modules)

By default, Quinoa will call the appropriate package manager install command (before building or starting) only if the node_modules directory doesn’t exist.

You may force a new installation using -Dquarkus.quinoa.force-install=true.

Quinoa will use the appropriate package manager frozen-lockfile command when installing, if the environment CI=true, or if quarkus.quinoa.frozen-lockfile=true. In this mode, the lockfile have to be present in the project.

Package manager commands

By default, the following commands and environment variables are used in the different faces for each of the supported package managers.


  • npm install (npm ci if quarkus.quinoa.frozen-lockfile=true).

  • pnpm install (pnpm install --frozen-lockfile if quarkus.quinoa.frozen-lockfile=true).

  • yarn install (yarn install --frozen-lockfile if quarkus.quinoa.frozen-lockfile=true).


(npm|pnpm|yarn) run build, with environment MODE=${mode} (dev, test or prod)


(npm|pnpm|yarn) test, with environment CI=true


(npm|pnpm|yarn) start, with environment BROWSER=none

Override package manager commands

By default, Quinoa uses sensible default commands when executing the different phases, install, build, test, dev. It is possible to override one or more of them from the package manager command configuration:

quarkus.quinoa.package-manager-command.install=npm ci --cache $CACHE_DIR/.npm --prefer-offline (1)
quarkus.quinoa.package-manager-command.build-env.BUILD=value (2)
1 This makes npm ci --cache $CACHE_DIR/.npm --prefer-offline the command executed in the install phase. (overriding quarkus.quinoa.package-manager and quarkus.quinoa.frozen-lockfile=true).
2 set environment variable BUILD with value value. Environment variables set in config can be added to the listed commands.
Using custom commands will override quarkus.quinoa.package-manager and quarkus.quinoa.frozen-lockfile.

UI live-coding dev server (proxy mode)

To enable the UI live-coding dev server, set a start script and set the port in the app config. Quinoa will transparently proxy relevant requests to the given port:

Quinoa relies on the dev server returning a 404 when the file is not found (See How it works). This is not the case on some dev servers configured with SPA routing. Make sure it is disabled in the dev server configuration (for React Create App, see #91). Another option, when possible, is to use [quarkus-quinoa_quarkus.quinoa.ignored-path-prefixes].


App created by Create React App (https://create-react-app.dev/docs/getting-started) are compatible without any change.

To enable React live coding server:



App created by ng (https://angular.io/guide/setup-local) require a tiny bit of configuration:


To enable Angular live coding server, you need to edit the package.json start script with ng serve --host --no-live-reload, then add this configuration:


If you want to use the Angular tests (instead of Playwright from the @QuarkusTest):

Change the package.json test script:

  "scripts": {
    "test": "ng test -- --no-watch --no-progress --browsers=ChromeHeadlessCI"

Edit the karma.conf.js:

  browsers: ['Chrome', 'ChromeHeadless', 'ChromeHeadlessCI'],
  customLaunchers: {
    ChromeHeadlessCI: {
      base: 'ChromeHeadless',
      flags: ['--no-sandbox']

Single Page application routing

Client-side/Browser/SPA routing is the internal handling of a route from the javascript in the browser. It uses the HTML5 History API

When enabled, to allow SPA routing, all relevant requests will be internally re-routed to index.html, this way the javascript can take care of the route inside the web-application.

To enable Single Page application routing:

By default, Quinoa will ignore quarkus.resteasy-reactive.path, quarkus.resteasy.path and quarkus.http.non-application-root-path path prefixes. You can specify different path prefixes to ignore using quarkus.quinoa.ignored-path-prefixes.
Currently, for technical reasons, the Quinoa SPA routing configuration won’t work with RESTEasy Classic. Instead, you may use a workaround (if your app has all the rest resources under the same path prefix):
public class SPARouting {
    private static final String[] PATH_PREFIXES = { "/api/", "/q/" };
    private static final Predicate<String> FILE_NAME_PREDICATE = Pattern.compile(".*[.][a-zA-Z\\d]+").asMatchPredicate();

    public void init(@Observes Router router) {
        router.get("/*").handler(rc -> {
            final String path = rc.normalizedPath();
            if (!path.equals("/")
                    && Stream.of(PATH_PREFIXES).noneMatch(path::startsWith)
                    && !FILE_NAME_PREDICATE.test(path)) {
            } else {

Http Headers

It’s very common to set up headers for caching on static resources, for example React proposes this configuration:

To configure Quarkus with those headers :


Http Compression

To enable server Http compression:



By default, the Web UI is not build/served in @QuarkusTest. The goal is to be able to test your api without having to wait for the Web UI build.

Quinoa features a testing library to make it easier to test your Web UI:


In order to enable the Web UI (build and serve) in a particular test, you can use the Enable test profile:

public class MyWebUITest {
    public void someTest() {

If you also want to run the tests included in your Web UI (i.e npm test) alongside this class, you can use the EnableAndRunTests test profile:

The library also brings a very elegant way to do e2e testing using Playright:

import com.microsoft.playwright.BrowserContext;
import com.microsoft.playwright.Page;
import com.microsoft.playwright.Response;
import io.quarkiverse.quinoa.testing.QuarkusPlaywrightManager;
import io.quarkiverse.quinoa.testing.QuinoaTestProfiles;
import io.quarkus.test.common.QuarkusTestResource;
import io.quarkus.test.common.http.TestHTTPResource;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.TestProfile;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import java.net.URL;

public class MyWebUITest {
    BrowserContext context;

    URL url;

    void name() {
        final Page page = context.newPage();
        Response response = page.navigate(url.toString());
        Assertions.assertEquals("OK", response.statusText());


        String title = page.title();
        Assertions.assertEquals("My App", title);

        // Make sure the app content is ok
        String greeting = page.innerText(".quinoa");
        Assertions.assertEquals("Hello World", greeting);


Most CI images already include NodeJS. if they don’t, just make sure to install it alongside Maven/Gradle (and Yarn/PNPM if needed). Then you can use it like any Maven/Gradle project.

Quinoa can be configured to install packages with a frozen lockfile.

On compatible CIs, don’t forget to enable the Maven/Gradle and NPM/Yarn repository caching.

Extension Configuration Reference

Configuration property fixed at build time - All other configuration properties are overridable at runtime

Configuration property



Indicate if the extension should be enabled. Default is true if the Web UI directory exists and dev and prod mode. Default is false in test mode (to avoid building the Web UI during backend tests).


disabled in test mode

Path to the Web UI (node) root directory. If not set ${project.root}/src/main/webui/ will be used. otherwise the path will be considered relative to the project root.



Path of the directory which contains the Web UI built files (generated during the build). After the build, Quinoa will take the files from this directory, move them to target/quinoa-build (or build/quinoa-build with Gradle) and serve them at runtime. The path is relative to the Web UI path. If not set "build/" will be used



Name of the package manager binary. If not set, it will be auto-detected depending on the lockfile falling back to "npm". Only npm, pnpm and yarn are supported for the moment.


auto-detected with lockfile

Name of the index page. If not set, "index.html" will be used.



Indicate if the Web UI should also be tested during the build phase (i.e: npm test). To be used in a io.quarkus.test.junit.QuarkusTestProfile to have Web UI test running during a io.quarkus.test.junit.QuarkusTest Default is false.



Install the packages using a frozen lockfile. Don’t generate a lockfile and fail if an update is needed (useful in CI). If not set it is true if environment CI=true, else it is false.


true if environment CI=true

Force install packages before building. If not set, it will install packages only if the node_modules directory is absent or when the package.json is modified in dev-mode.



Enable SPA (Single Page Application) routing, all relevant requests will be re-routed to the "index.html". Currently, for technical reasons, the Quinoa SPA routing configuration won’t work with RESTEasy Classic. If not set, it is disabled.



List of path prefixes to be ignored by Quinoa. If not set, "quarkus.resteasy-reactive.path", "quarkus.resteasy.path" and "quarkus.http.non-application-root-path" will be ignored.

list of string

Enable external dev server (live coding). The dev server process (i.e npm start) is managed like a dev service by Quarkus. This defines the port of the server to forward requests to. If the external server responds with a 404, it is ignored by Quinoa and processed like any other backend request.


After start, Quinoa wait for the external dev server. by sending GET requests to this path waiting for a 200 status. If not set the default is "/". If empty string "", Quinoa will not check if the dev server is up.



Timeout in ms for the dev server to be up and running. If not set the default is ~30000ms



Enable external dev server live coding logs. This is not enabled by default because most dev servers display compilation errors directly in the browser. False if not set.



Custom command for installing all dependencies. e.g. «npm ci --cache $CACHE_DIR/.npm --prefer-offline»


Custom command for building the application.


Custom command for running tests for the application.


Custom command for starting the application in development mode.