Starter: My first web app


A step-by-step tutorial for building your first web application with Enonic XP


This guide will take you through the basic steps of creating a web app, using the basic building blocks of Enonic XP.

During this exercise you will:

  • learn about http controllers and view templates

  • optimize serving of static assets

  • make use of the router library

  • set up a vhost, and more..

Ready, set, code!

Create project

To setup a project locally, run the following command:

enonic project create -r starter-myfirstwebapp

Remember to create a new XP sandbox when completing the project wizard.

Don’t have the Enonic CLI? Visit the Getting started guide to install it.

Project structure

The project folder created in the previous step should now contain the following project structure:

build.gradle (1)
build/ (2)
    java/ (3)
    resources/ (4)
      assets/ (5)
      webapp/ (6)
1 The gradle files are used by the build system
2 Contains output files produced by the build
3 Optional folder for Java code (used to import sample site in this starter)
4 Main location for XP specific project code
5 Static assets such as css and icons are placed here
6 Folder containing the root webapp http controller

Building and Deploying

From the project folder created (i.e. myproject/), run this command:

enonic dev

enonic dev command will start your sandbox in detached dev mode, and also execute the dev gradle task on the myfirstwebapp starter, providing you a smooth experience while developing with Enonic. For further details on Enonic CLI’s dev mode, read the docs.


To see your current application:

  1. log in to the XP admin console (http://localhost:8080)

  2. open the "Applications" app, and select the listed applications

  3. visit the app by clicking the web app link.

JS Controller

The essense of a web application is the controller. In your project, you will find the following file:

exports.get = function (req) {
  var title = 'Hello Web app';
  return  {
  body: `
    <link rel="stylesheet" type="text/css" href="styles.css"/>
      <h1>Sweet, "${title}" is working!</h1>
      <img src="html5logo.svg"/>

This controller file is automatically executed when your web app is being accessed.

The example above is also referencing two asset files. These files can be found in the src/main/resources/assets/ folder of your project.

By default, the web app engine automatically serves files placed in the asset/ as if they were located in the root of your web app.

Using Views

According to the MVC (Model View Controller) pattern, we should separate the View (template) from the controller. Enonic XP supports a variety of templating engines. In this step, we’ll use Thymeleaf:

  1. To make sure Thymeleaf is available for our project, we must update the build.gradle file. Uncomment the following line from the "dependencies" section:

    include "com.enonic.lib:lib-thymeleaf:2.0.0"
  2. Create a new file hello.html and add it to the webapp/ folder

        <title data-th-text="${title}">Dummy title</title>
        <link rel="stylesheet" type="text/css" data-th-href="styles.css" href="../assets/styles.css"/>
          <h1 data-th-text="'Sweet... ' + ${message}">Dummy heading</h1>
          <img data-th-src="html5logo.svg" src="../assets/html5logo.svg"/>
  3. Then update your controller file to use the new template:

    var thymeleaf = require('/lib/thymeleaf'); // Load template engine
    var VIEW = resolve('hello.html') // Lookup template file
    exports.get = function (req) {
      var model = { // Build model object
        title: 'Hello Web app',
        message: 'Views are working too!'
      return {
        body: thymeleaf.render(VIEW, model) // Render page
Notice the VIEW "constant". Things that don’t change can be set outside the get function.

Refresh your app, and you should see the heading "Sweet…​ Views are working too!".

A cool feature related to Thymeleaf templates is that they are actually pure HTML. Meaning they may be opened directly in your browser. Try opening the hello.html file to see for yourself:

Screenshot of template showing dummy heading and html5 logo

Asset serving

Our approach asset serving is simple, but not optimal. XP also provides a more optimized solution for serving assets where:

  • Asset url are created automatically (no need to deal with relative paths)

  • Assets get "infinite" cache headers (used by proxies and browsers)

  • Every time you deploy a new app, new url’s are generated (no more stale assets)

To use this functionality, we simply need to update our view template:

  1. Update hello.html template with the following content:

        <title data-th-text="${title}"></title>
        <link rel="stylesheet" type="text/css" data-th-href="${portal.assetUrl({'_path=styles.css'})}"/>
          <h1 data-th-text="'Faster assets... '+ ${message}"></h1>
          <img data-th-src="${portal.assetUrl({'_path=html5logo.svg'})}"/>
    Notice the dummy values and attributes are removed. They are only needed if you want to open the Thymeleaf template directly in a browser.

    After refreshing your app and inspecting the html5 logo, you should now see something like this:

    <img src="/webapp/webapp.demo/_/asset/webapp.demo:1557487230/html5logo.svg">
Check out the "Network" tab in your browsers dev tools to see the cache headers


Routing, or handling different URLs within your app is a common requirement for web applications.

In this step we will create a basic server-side router. In addition to the start page, we will create two more pages and handle navigation between them.

  1. This time we will use the router library. Add the following line to the dependencies section of the build.gradle file to make it available.

    include "com.enonic.lib:lib-router:3.1.0"
  2. Then update your webapp.js and hello.html as follows:

    var thymeleaf = require('/lib/thymeleaf'); // Load template engine
    var router = require('/lib/router')(); // Load router library
    router.get(['', '/'], function() { return renderPage('Routing FTW'); } );
    router.get('/page', function() { return renderPage('Gone to page, you have'); } );
    router.get('/page/subpage', function() { return renderPage('Gone to sub page indeed, you have'); } );
    function renderPage(message) {
      var model = {
        title: 'Hello router',
        message: message
      return  {
        body: thymeleaf.render(resolve('hello.html'), model)
    exports.get = function (req) {
        return router.dispatch(req);
          <a th:href="${portal.pageUrl({'_path=/'})}">Main</a>
          <a th:href="${portal.pageUrl({'_path=page'})}">Page</a>
          <a th:href="${portal.pageUrl({'_path=page/subpage'})}">Subpage</a>
        <img th:src="${portal.assetUrl({'_path=html5logo.svg'})}"/>

    If you don’t need to validate your Thymeleaf template as pure html you can:

    • use inlined expressions [[${variable}]] rather than data-th-text="${variable}"

    • use shorthand th:href rather than data-th-href

    Reload and enjoy navigating between the pages.

Notice the use of pageUrl() to dynamically generate server relative URLs for the pages. This eliminates the need for dealing with relative links etc.

Setting up a vhost

Our webapp URL does not look like something we would want in production. XP provides a concept called Vhosts to map the internal XP URI to a public facing URL i.e.

  1. Start by locating your sandbox' config folder. It is placed within your users home folder at .enonic/sandboxes/<name-of-sandbox>/home/config/

  2. Update the com.enonic.xp.web.vhost.cfg file in the config/ folder as follows:

    enabled = true
    # Vhost to test our new web app =
    mapping.mywebapp.source = / = /webapp/com.example.myproject/
    # Still access everything on "localhost" = localhost
    mapping.lhost.source = / = /
    mapping.lhost.idProvider.system = default
    Remember to update the value of to match the URI to your webapp i.e. "/webapp/"
  3. After saving the vhost config file, you should see the following line the XP Sandbox log:

    2019-05-10 11:34:17,234 INFO  c.e.x.w.v.i.c.VirtualHostConfigImpl - Virtual host is enabled and mappings updated.
  4. Finally, a small trick to fool our browser into thinking is pointing to your local machine. Add the following line to your hosts file.
    On Mac/Linux_: /etc/hosts, on Windows: c:\Windows\System32\Drivers\etc\hosts
  5. Point your browser to to see the glorious result.

Read more about vhost configuration in the XP docs.


While developing an app, it can be helpful to do some logging. Try adding the following line into the exports.get section of your webapp controller and see what happens:'Testing logging: %s', JSON.stringify(req, null, 4));
A simplified logging function and many more are included in the Util Library