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)

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 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. |
Prerequisite
-
Create or use an existing Quarkus application
-
Add the Quinoa extension
-
Install NodeJS (https://nodejs.org/) or make sure Quinoa is configured to install it.
Installation
Create a new Quinoa project (with a base Quinoa starter code):
-
With code.quarkus.io
-
With the Quarkus CLI:
quarkus create app quinoa-app -x=io.quarkiverse.quinoa:quarkus-quinoa
Then start the live-coding:
quarkus dev
And navigate to http://0.0.0.0:8080/quinoa.html
You could also just add the extension (but you won’t get the starter code):
-
With the Quarkus CLI:
quarkus ext add io.quarkiverse.quinoa:quarkus-quinoa
-
In your
pom.xml
file, add this dependency:
<dependency>
<groupId>io.quarkiverse.quinoa</groupId>
<artifactId>quarkus-quinoa</artifactId>
<version>2.0.2</version>
</dependency>
Getting Started
If not yet created by the tooling, you will need 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 long as the files are available at build time.
quarkus.quinoa.ui-dir=../my-webui
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 thepackage.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 development from production builds using the environment variable NODE_ENV
(production
/development
). by-node-env can help you if you have different build commands:
"scripts": {
"build": "by-node-env",
"build:development": "...",
"build:production": "...",
},
"devDependencies": {
"by-node-env": "~2.0.1"
}
Package manager
Quinoa can be configured to install NodeJS and NPM in the project directory:
quarkus.quinoa.package-manager-install=true (1)
quarkus.quinoa.package-manager-install.node-version=16.17.0 (2)
1 | Enable package manager install |
2 | Define the version of NodeJS to install |
By default, NodeJS and NPM will be installed in {project-dir}/.quinoa/ (can be configured). If not specified, it will use the NPM version provided by NodeJS.
|
If NodeJS and NPM are not installed by Quinoa, it is possible to override the package manager (NPM, Yarn or PNPM), otherwise, 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
By default, 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.
Install:
-
npm install
(npm ci
ifquarkus.quinoa.frozen-lockfile=true
). -
pnpm install
(pnpm install --frozen-lockfile
ifquarkus.quinoa.frozen-lockfile=true
). -
yarn install
(yarn install --frozen-lockfile
ifquarkus.quinoa.frozen-lockfile=true
).
Build:
(npm|pnpm|yarn) run build
, with environment MODE=${mode}
(dev
, test
or prod
)
Test:
(npm|pnpm|yarn) test
, with environment CI=true
Dev:
(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 .
|
if NodeJS is installed by Quinoa, you need to enable: quarkus.quinoa.package-manager-command.prepend-binary and adapt the command to only specify the arguments (the binary to call will be prepended by Quinoa).
|
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:
quarkus.quinoa.dev-server.port=3000
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]. |
React
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:
quarkus.quinoa.dev-server.port=3000
Angular
App created by ng
(https://angular.io/guide/setup-local) require a tiny bit of configuration:
quarkus.quinoa.build-dir=dist/[your-app-name]
To enable Angular live coding server, you need to edit the package.json start script with ng serve --host 0.0.0.0 --disable-host-check
, then add this configuration:
quarkus.quinoa.dev-server.port=4200
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']
}
},
Next.js
Any app created with Next.js (https://nextjs.org/) should work with Quinoa after the following changes:
In application.properties add:
%dev.quarkus.quinoa.index-page=/
quarkus.quinoa.build-dir=out
In Dev mode Next.js serves everything out of root "/" but in PRD mode its the normal "/index.html".
Add these scripts to package.json
"scripts": {
...
"start": "next dev",
"build": "next build && next export",
}
Vite
Any app created with Vite (https://vitejs.dev/guide/) should work with Quinoa after the following changes:
In application.properties add:
quarkus.quinoa.dev-server.port=5173
quarkus.quinoa.build-dir=dist
Add start script to package.json
"scripts": {
...
"start": "vite"
},
Hot Module Replacement (HMR) should work by default.
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:
quarkus.quinoa.enable-spa-routing=true
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): |
@ApplicationScoped
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)) {
rc.reroute("/");
} else {
rc.next();
}
});
}
}
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 :
quarkus.http.filter.others.header.Cache-Control=no-cache
quarkus.http.filter.others.matches=/.*
quarkus.http.filter.others.methods=GET
quarkus.http.filter.others.order=0
quarkus.http.filter.static.header.Cache-Control=max-age=31536000
quarkus.http.filter.static.matches=/static/.+
quarkus.http.filter.static.methods=GET
quarkus.http.filter.static.order=1
Testing
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:
<dependency>
<groupId>io.quarkiverse.quinoa</groupId>
<artifactId>quarkus-quinoa-testing</artifactId>
<version>2.0.2</version>
<scope>test</scope>
</dependency>
In order to enable the Web UI (build and serve) in a particular test, you can use the Enable
test profile:
@QuarkusTest
@TestProfile(QuinoaTestProfiles.Enable.class)
public class MyWebUITest {
@Test
public void someTest() {
// your test logic here
}
}
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:
@QuarkusTest
@TestProfile(QuinoaTestProfiles.EnableAndRunTests.class)
public class AllWebUITest {
@Test
public void runTest() {
// you don't need anything here, it will run your package.json "test"
}
}
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;
@QuarkusTest
@TestProfile(QuinoaTestProfiles.Enable.class)
@QuarkusTestResource(QuarkusPlaywrightManager.class)
public class MyWebUITest {
@QuarkusPlaywrightManager.InjectPlaywright
BrowserContext context;
@TestHTTPResource("/")
URL url;
@Test
void name() {
final Page page = context.newPage();
Response response = page.navigate(url.toString());
Assertions.assertEquals("OK", response.statusText());
page.waitForLoadState();
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);
}
}
CI
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
Type |
Default |
|
---|---|---|
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). Environment variable: |
boolean |
|
Indicate if Quinoa should just do the build part. If true, Quinoa will NOT serve the Web UI built resources. This is handy when the output of the build is used to be served via something else (nginx, cdn, …) Quinoa put the built files in 'target/quinoa-build' (or 'build/quinoa-build with Gradle). Default is false. Environment variable: |
boolean |
|
Path to the Web UI (NodeJS) root directory. If not set ${project.root}/src/main/webui/ will be used. otherwise the path will be considered relative to the project root. Environment variable: |
string |
|
This the Web UI internal build system (webpack, …) output directory. 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 Environment variable: |
string |
|
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. Environment variable: |
string |
|
Enable Package Manager Installation. This will override "package-manager" config. Set "quarkus.quinoa.package-manager-command.prepend-binary=true" when using with custom commands Environment variable: |
boolean |
|
The directory where NodeJS should be installed, it will be installed in a node/ sub-directory. Default is ${project.root}/.quinoa Environment variable: |
string |
|
The NodeJS Version to install locally to the project. Required when package-manager-install is enabled. Environment variable: |
string |
|
Where to download NodeJS from. Environment variable: |
string |
|
The NPM version to install. By default, the version is provided by NodeJS. Environment variable: |
string |
|
Where to download NPM from. Environment variable: |
string |
|
The PNPM version to install. If the version is set and NPM and YARN are not set, then this version will attempt to be downloaded. Environment variable: |
string |
|
Where to download PNPM from. Environment variable: |
string |
|
The YARN version to install. If the version is set and NPM Version is not set, then this version will attempt to be downloaded. Environment variable: |
string |
|
Where to download YARN from. Environment variable: |
string |
|
If true, the package manager binary will be prepended by Quinoa (Only configure the arguments in the different commands as the binary will be prepended). e.g. «quarkus.quinoa.package-manager-command.install=ci --cache $CACHE_DIR/.npm --prefer-offline» Else, the command should also contain the binary. Environment variable: |
boolean |
|
Custom command for installing all dependencies. e.g. «npm ci --cache $CACHE_DIR/.npm --prefer-offline» Environment variable: |
string |
|
Custom command for building the application. Environment variable: |
string |
|
Custom command for running tests for the application. Environment variable: |
string |
|
Custom command for starting the application in development mode. Environment variable: |
string |
|
Name of the index page. If not set, "index.html" will be used. Environment variable: |
string |
|
Indicate if the Web UI should also be tested during the build phase (i.e: npm test). To be used in a Environment variable: |
boolean |
|
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. Environment variable: |
boolean |
|
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. Environment variable: |
boolean |
|
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. Environment variable: |
boolean |
|
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. Environment variable: |
list of string |
|
Enable external dev server (live coding). The "dev-server.port" config is required to communicate with the dev server. If not set the default is true. Environment variable: |
boolean |
|
When set to true, Quinoa will manage the Web UI dev server When set to false, the Web UI dev server have to be started before running Quarkus dev Environment variable: |
boolean |
|
Port of the server to forward requests to. The dev server process (i.e npm start) is managed like a dev service by Quarkus. If the external server responds with a 404, it is ignored by Quinoa and processed like any other backend request. Environment variable: |
int |
|
Host of the server to forward requests to. "localhost" is the default Environment variable: |
string |
|
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. Environment variable: |
string |
|
By default, Quinoa will handle request upgrade to websocket and act as proxy with the dev server. If set to false, Quinoa will pass websocket upgrade request to the next Vert.x route handler. Environment variable: |
boolean |
|
Timeout in ms for the dev server to be up and running. If not set the default is ~30000ms. Environment variable: |
int |
|
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. Environment variable: |
boolean |
|
Environment variable: |
|
|
Environment variable: |
|
|
Environment variable: |
|
|
Environment variable: |
|