Fork me on GitHub

JavalinVue Plugin

The JavalinVue plugin provides a very clever integration with Vue.js. As with most clever programming tricks, you will probably either love it or hate it. These docs are only valid for Javalin 4.X.

How does it work?

The JavalinVue plugin is basically a very specialized templating engine. It finds .vue (and optionally .js and .css) files and glues them together, and serves it all as one big HTML file. You start by creating a layout file:

    <script src="/webjars/vue/2.6.10/dist/vue.min.js"></script>
    <style>@inlineFile("/vue/styles.css")</style> <!-- You can inline specific js/css files -->
    @componentRegistration <!-- JavalinVue will find required vue files and inline them here -->
<main id="main-vue" v-cloak>
    @routeComponent <!-- Your route component will be inlined here (app.get("/my-page", VueComponent("my-page"))) -->
    new Vue({el: "#main-vue"});

When a user tries to access /my-page in their browser, JavalinVue will serve the following HTML:

    <script src="/webjars/vue/2.6.10/dist/vue.min.js"></script>
    <style>body{background:red}</style> <!-- whatever was in styles.css -->
    <!-- <my-page></my-page> component and all of its dependencies -->
<main id="main-vue" v-cloak>
    <my-page></my-page> <!-- this was defined in app.get("/my-page", VueComponent("my-page")) -->
    new Vue({el: "#main-vue"});

You don’t need any frontend build tool (like Webpack, Parcel, Grunt, etc) – JavalinVue takes care of all that. As a consequence import/export of ES modules is not needed (and not supported as of now).

There is a longer tutorial which includes the motivation behind creating this integration, as well as some discussion about pros and cons: /tutorials/simple-frontends-with-javalin-and-vue

Getting Started

Creating a layout

Create a root directory for your vue files and place your layout.html in it. By default, JavalinVue will look in src/main/resources/vue:


Your layout.html file will be responsible for initializing Vue and including all your dependencies. The snippet below shows all the available macros (@macroName):

    <script src="@cdnWebjar/vue/2.6.10/dist/vue.min.js"></script>
    <style>@inlineFile("/vue/styles.css")</style> <!-- always included -->
    <script>@inlineFileDev("/vue/scripts-dev.js")</script> <!-- only included in dev -->
    <script>@inlineFileNotDev("/vue/scripts-not-dev.js")</script> <!-- only included in not dev -->
    @componentRegistration <!-- JavalinVue will find required vue files and inline them here -->
<main id="main-vue" v-cloak>
    @routeComponent <!-- Your route component will be inlined here (app.get("/my-page", VueComponent("my-page"))) -->
    new Vue({el: "#main-vue"});

Creating a component

Components will be inlined where the @componentRegistration macro is present in your layout.html, which means you have to register them as global Vue components:

<template id="my-component">
        <!-- Component code goes here -->
    Vue.component("my-component", {
        template: "#my-component"

This component will now be available to be called from any other component you make, or as a @routeComponent.

Binding to a route

Routing is done server side, so you bind a component to a route by declaring a GET endpoint in Javalin:

app.get("/my-path", VueComponent("my-component"))

This means that you can use the same AccessManager for frontend routes as you use for your API:

app.get("/my-path", VueComponent("my-component"), roles(Role.LOGGED_IN))

Configuration options


By default, JavalinVue will set the root directory based on the first request it serves.

This is done to make development fast locally, and requests fast in production. If you set the root dir explicitly, Javalin won’t try to guess what to do:

JavalinVue.rootDirectory(c -> c.classpathPath("/path")); // use the path on the same classpath as Javalin
JavalinVue.rootDirectory(c -> c.classpathPath("/path", MyClass.class)); // use the path on the classpath of provided Class
JavalinVue.rootDirectory(c -> c.externalPath("/path")); // use an external path
JavalinVue.rootDirectory(c -> c.explicitPath(path)); // use an explicit Path object
JavalinVue.rootDirectory { it.classpathPath("/path") } // use the path on the same classpath as Javalin
JavalinVue.rootDirectory { it.classpathPath("/path", MyClass.class) } // use the path on the classpath of provided Class
JavalinVue.rootDirectory { it.externalPath("/path") } // use an external path
JavalinVue.rootDirectory { it.explicitPath(path) } // use an explicit Path object


By default, version is set to Vue 2. If you’re using Vue 3 you can configure that:

JavalinVue.vueVersion(c -> c.vue2());
JavalinVue.vueVersion(c -> c.vue3("VueAppName"));
JavalinVue.vueVersion { it.vue2() }
JavalinVue.vueVersion { it.vue3("VueAppName") }


If you want to share state from your server with Vue, you can provide JavalinVue with a state function:

JavalinVue.stateFunction = ctx -> Map.of("user", getUser(ctx));
JavalinVue.stateFunction = { mapOf("user" to getUser(it)) }

This can then be accessed from the state variable:

<template id="user-template">
    <div>{{ $javalin.state.user }}</div>

The function runs for every request, so the state is always up to date when the user navigates or refreshes the page.


By default, this isDevFunction is set to check if the request host is "localhost" or "". This function is called once on the first request JavalinVue sees, and is used to set an isDev property, which is then used to make decisions on how to build the HTML for a request.

JavalinVue.isDevFunction = ctx -> // your code here
JavalinVue.isDevFunction { /* Your code here */ }


By default, this is set to true. If you set it to false, every .vue file that JavalinVue finds will be inlined in @componentRegistration. If you leave it as true, only required .vue files will be included.

JavalinVue.optimizeDependencies = true/false;


By default, JavalinVue sets the "Cache-Control" header to "no-cache, no-store, must-revalidate". This can be configured:

JavalinVue.cacheControl = "...";

Layout macros

@inlineFile("/path/to/file.ext")        // this file will always be inlined
@inlineFileDev("/path/to/file.ext")     // this file will be inlined if JavalinVue.isDev is true
@inlineFileNotDev("/path/to/file.ext")  // this file will be inlined if JavalinVue.isDev is false
@componentRegistration                  // all required components will be inlined here
@routeComponent                         // the current route component will be inlined here
@cdnWebjar                              // will resolve to webjar path if dev, cdn if not dev


You can reference your WebJars with @cdnWebjar/ instead of the normal /webjars/. If you do this, the path will resolve to /webjars/ on when isDevFunction returns true, and https// on non-localhost. Note that this only works with NPM webjars.


The JavalinVue plugin includes a small class for making HTTP get-requests to your backend, it can be used like this:

<template id="books-component">
        <div v-if="books.loading">Loading books ...</div>
        <div v-if="books.loadError">Failed to load books! ({{books.loadError.text}})</div>
        <div v-if="books.loaded" v-for="book in">{{book}}</div>
    Vue.component("books-component", {
        template: "#books-component",
        data: () => ({
            books: new LoadableData("/api/books"),

The class automatically caches the request in localStorage, so subsequent requests will appear to load instantly. All configuration options and methods are shown below:

const useCache = true/false;
const errorCallback = error => alert(`An error occurred! Code: ${error.code}, text: ${error.text}`);

// Create a new instance with config options
const loadableData = new LoadableData("/api/books", useCache, errorCallback);
// Refresh data (can use cache to avoid flickering)
loadableData.refresh(useCache, errorCallback);

// Refresh data for other instances which uses the same endpoint
let users = new LoadableData("/users");
let sameUsers = new LoadableData("/users"); // this variable could be in a different component
users.refreshAll(); // sameUsers will also be refreshed, since they share the same URL

// Refresh data via static method
LoadableData.refreshAll("/users"); // all instances with this URL will refresh themselves

The loadError object contains the HTTP status and error message, and is available in both the template and in the error callback function.

Good to know

JavalinVue will also put path-parameters in the Vue instance, which you can access like this:

<template id="thread-view">
    <div>{{ $javalin.pathParams["user"] }}</div>
    Vue.component("thread-view", {
        template: "#thread-view"

Local state

This feature came to life because someone was abusing JavalinVue.stateFunction by overwriting it for every request. If you find yourself doing this, you should either rewrite your app to fetch data using LoadableData (recommended), or use local state for VueComponent (usually not recommended).

Every VueComponent can take a state object as a second parameter. This state overwrites the state from stateFunction. You can either add it directly in a route declaration:

app.get("/specific-state", VueComponent("test-component", mapOf("test" to "tast")))

Or inside a handler:

app.get("/specific-state") { ctx ->
    val myState = mapOf("test" to "tast")
    VueComponent("test-component", myState).handle(ctx)

Note that this is something that you CAN do it, not something that you SHOULD do. Only do this if you have thought hard about it, and it solves a real problem for you.

Like Javalin?
Star us 😊