Javalin Logo

Javalin Cookbook

Begin creating a new project from scratch using this tutorial. Then you might need to know how to …

Javalin

  1. How to organize files
  2. How to add more routes
  3. How to add a database
  4. How to export and import a Postgres database

Thymeleaf

  1. How to re-use fragments in templates
  2. How to use fragments with parameters
  3. How to add an image
  4. How to iterate through a list (each ….)
  5. How to use conditionals (if …)
  6. How to add a link

Javalin & Thymeleaf

  1. How to transfer data between frontend and backend

Javalin

1. How to organize files

It’s generally a good idea to organize the Java App into meaningful packages. Suggested packages are:

  • config (configuration files, Thymeleaf etc)
  • controllers (first stop for each route, these can call service methods, facade DB methods etc)
  • services (domain / business logic and methods, can also call facade DB methods)
  • entities (domain classes that mirrors the DB)
  • exceptions (custom exceptions)
  • persistence (database related mapper classes and database facade classes)
  • resources
    • public
      • css
      • images
    • templates

2. How to add more routes

GET og POST request can be added like this:

app.get("/users", ctx -> UserController.showUserList(ctx, connectionPool));
app.post("/login", ctx -> UserController.login(ctx, connectionPool));

In this example a Class UserController has been created to organize the code. The methods showUserList and login are made static for ease of use.

3. How to add a database

Add a thread-safe connection pool with 2-10 JDBC Postgresql connections. Create a package called persistence and drop the ConnectionPool class and the rest of DB classes into it:

package app.persistence;

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.logging.Level;
import java.util.logging.Logger;

/***
 * Singleton pattern applied to handling a Hikari ConnectionPool
 */
public class ConnectionPool {

    private static volatile ConnectionPool instance = null;
    private static HikariDataSource ds = null;
    private static final Logger LOGGER = Logger.getLogger(ConnectionPool.class.getName());

    /***
     * Private constructor to enforce Singleton pattern.
     */
    private ConnectionPool() {
        // Prevent instantiation
    }

    /***
     * Getting a singleton instance of a Hikari Connection Pool with specific credentials
     * and connection string. If an environment variable "DEPLOYED" exists, then environment variables
     * will be used instead of provided parameters.
     * @param user Database username
     * @param password Database password
     * @param url Database connection URL
     * @param db Database name
     * @return Singleton instance of ConnectionPool
     */
    public static ConnectionPool getInstance(String user, String password, String url, String db) {
        if (instance == null) {
            synchronized (ConnectionPool.class) {
                if (instance == null) {  // Double-checked locking
                    if (System.getenv("DEPLOYED") != null) {
                        ds = createHikariConnectionPool(
                                System.getenv("JDBC_USER"),
                                System.getenv("JDBC_PASSWORD"),
                                System.getenv("JDBC_CONNECTION_STRING"),
                                System.getenv("JDBC_DB"));
                    } else {
                        ds = createHikariConnectionPool(user, password, url, db);
                    }
                    instance = new ConnectionPool();
                }
            }
        }
        return instance;
    }

    /***
     * Getting a live connection from the Hikari Connection Pool
     * @return a database connection
     * @throws SQLException if connection fails
     */
    public Connection getConnection() throws SQLException {
        if (ds == null) {
            throw new SQLException("DataSource is not initialized. Call getInstance() first.");
        }
        return ds.getConnection();
    }

    /***
     * Closing the Hikari Connection Pool
     */
    public void close() {
        if (ds != null) {
            LOGGER.log(Level.INFO, "Shutting down connection pool...");
            ds.close();
            ds = null;
            instance = null;
        }
    }

    /***
     * Configuring a Hikari DataSource ConnectionPool
     * @param user Database username
     * @param password Database password
     * @param url Database connection URL
     * @param db Database name
     * @return Configured HikariDataSource
     */
    private static HikariDataSource createHikariConnectionPool(String user, String password, String url, String db) {
        LOGGER.log(Level.INFO, "Initializing Connection Pool for database: {0}", db);

        HikariConfig config = new HikariConfig();
        config.setDriverClassName("org.postgresql.Driver");
        config.setJdbcUrl(String.format(url, db));
        config.setUsername(user);
        config.setPassword(password);

        // Connection Pool Configurations
        config.setMaximumPoolSize(10); // Default is 10
        config.setMinimumIdle(2);      // Ensures some connections are always available
        config.setIdleTimeout(30000);  // 30 seconds idle timeout
        config.setConnectionTimeout(30000); // Max wait time for a connection
        config.setPoolName("Postgresql-Pool");

        // Optimizations
        config.addDataSourceProperty("cachePrepStmts", "true");
        config.addDataSourceProperty("prepStmtCacheSize", "250");
        config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");

        return new HikariDataSource(config);
    }
}

Add to beginning of Main-class:

private static final Logger LOGGER = Logger.getLogger(Main.class.getName());

private static final String USER = "postgres";
private static final String PASSWORD = "postgres";
private static final String URL = "jdbc:postgresql://localhost:5432/%s?currentSchema=public";
private static final String DB = "databasename";

private static final ConnectionPool connectionPool = ConnectionPool.getInstance(USER, PASSWORD, URL, DB);

Then the database can be accessed like this:

public static List<User> getAllUsers(ConnectionPool connectionPool) throws DatabaseException
    {
        List<User> userList = new ArrayList<>();
        String sql = "select * from users";
        try (Connection connection = connectionPool.getConnection())
        {
            try (PreparedStatement ps = connection.prepareStatement(sql))
            {
                ResultSet rs = ps.executeQuery();

                while (rs.next())
                {
                    String userName = rs.getString("username");
                    String password = rs.getString("password");
                    String role = rs.getString("role");
                    User user = new User(userName, password, role);
                    userList.add(user);
                }
            }
        }
        catch (SQLException ex)
        {
            throw new DatabaseException(ex, "Could not get users from database");
        }
        return userList;
    }

4. How to export and import a database

This is how you share a database between team members (when not having a shared database in the cloud):

  1. Right-click your database in PG-Admin and choose Backup ...
  2. Find a filename. Use same name as database cupcake
  3. Format: Custom (the most efficient)
  4. Encoding: UTF8
  5. Data Option Tab: select Pre-data, Data, and Post-data. These three ensure that everything is copied.
  6. Once the backup is done. Go to the topmenu, and choose: Tools -> Store manager ...
  7. Pick the backup file you just created in click the download button.
  8. Find the backup file in your download folder on disk, and copy it to your team members.

This is how you import / restore the file on another machine

  1. Create a new database in PG-Admin with the same name as the back-up’ed version
  2. Right-click the new database and choose Restore ...
  3. Choose Custom or tar as Format
  4. Click the folder icon in the Filename to open the file picker
  5. Click the 3 small dots in the upper right corner to open a local menu, and then click Upload
  6. Drop the cupcake file into the file picker to upload - cancel to get back to the file list, and then mark the cupcake file
  7. Click Restore to import
  8. Refresh the database to see the newly imported database and data

Thymeleaf

1. How to re-use fragments in templates

Re-use html fragments like this:

  • Create a html-file in the templates-folder to contain all the fragments. One file to rule them all. Call it fragments.html This is an example with a re-usable <head> fragment, a header-fragment, and a footer-fragment:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">

<head th:fragment="head(title)">
    <title th:text="${title}"></title>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link href="../public/css/styles.css" th:href="@{/css/styles.css}" rel="stylesheet"/>
</head>

<body>

    <header th:fragment="header" style="width:100%;border-bottom:1px solid black;">
        <img src="../public/images/fourthingsplus.png" th:src="@{/images/fourthingsplus.png}" width="100%"/>
    </header>

    <footer th:fragment="footer" style="width:100%;border-top:1px solid black;">
        Dette er så en footer.
    </footer>

</body>
</html>

These fragments can replace html-elements in another document like this:

<div th:replace="~{fragments :: head('Frontpage')}"></div>
<div th:replace="~{fragments :: header}"></div>
<div th:replace="~{fragments :: footer}"></div>

OBS! Use the fragments wisely. The upside of fragments is adhering to the DRY principle. The downside is destroying the Thymeleaf principle of Natural Templating. Natural Templating means that you can show your template in a normal browser. The browser cannot interpret fragments, so they spoil the party. You need to run the template through the Thymeleaf engine for fragments to work. And you need to compile and run your Javalin project to make that happen. This takes extra time, and makes it hard to delegate template building to frontend staff. A compromise could be to keep the <head> section on each template and then stuff menus, footers etc in fragments. If you put the <head> section in a fragment, you will miss importing stylesheets etc. Think about it. You might also reade this comment by one of the developers of Thymeleaf.

2. How to use fragments with parameters

In the fragment-example above - notice how the text ‘Frontpage’ is passed as an argument to the fragment. Also notice how the parameter is used in the fragment: <title th:text="${title}"></title>

3. How to add an image

To insert an image do like this:

 <img src="../public/images/fourthingsplus.png" th:src="@{/images/fourthingsplus.png}" width="100%"/>

Note: place the image file in the /public/images folder. The th:src="@{/images/fourthingsplus.png}" attribute is used by Thymeleaf. The other attribute: src="../public/images/fourthingsplus.png" will be applied in case you open the file directly in a browser instead of through the Thymeleaf template engine.

4. How to iterate through a list (each ….)

This example assumed that a list of user objects are passed from Javalin:

 <h1>Users</h1>
<table>
   <thead>
   <tr>
   <th>username</th>
   <th>password</th>
   <th>role</th>
   </tr>
   </thead>
   <tbody>
   <tr th:each="user : ${userList}">
      <td th:text="${user.username}">username</td>
      <td th:text="${user.password}">password</td>
      <td th:text="${user.role}">role</td>
   </tr>
   </tbody>
</table>

That’s how the th:each attribute works. It’s is important that you spell the attribute names correctly. username, password, and role should be attributes in the Java User class.

5. How to use conditionals (if …)

Check for details in the Thymeleaf docs.

An example of how to apply if:

  <p th:if="${not #lists.isEmpty(userList)}">Listen er ikke tom</p>

Another way is to use the ternary operator If-then-else: (if) ? (then) : (else) like this:

<p th:text="${not #lists.isEmpty(userList) ? userList.size + ' in list' : 'Den er tom' }">Ikke tom</p>

The link (url/route) should be given in the th:href attribute. In this example as th:href="@{/createuser}". It means that the navigation will be forwarded to /createuser. However, if you want to open the html document in a browser directly, then the first link will work: href="createuser.html".

<a href="createuser.html" th:href="@{/createuser}">Create account</a>

Javalin & Thymeleaf

1. How to transfer data between frontend and backend

To make the website dynamic means to adapt the html pages to user-input.

1.a From backend to frontend

Use a hashmap to add objects you want to transfer to a Thymeleaf template. Add to the built-in maps in the ctx object:

ctx maps:

  • Session Scope
ctx.sessionAttribute("userList", userList);
  • Request Scope
ctx.attribute("totalSum", 1025);

And then render the template including your attributes:

ctx.render("index.html");

From HTML and Thymeleaf, the hashmaps are accessible in various ways.

  • Session Scope
<p th:text="${session.email}"></p>
  • Request Scope
<p th:text="${name}"></p>

Note: unfortunately, IntelliJ is not able to provide autocompletion and name resolution for scope and model variables. Luckily it works anyway. A work-a-round is to define the variables as Thymeleaf variables in the beginning of the body block:

<body>
   <!--@thymesVar id="hello" type="String"-->
   <div th:text="${hello}"></div>
</body>

In this way we get autocompletion. But it’s probably not worth it.

1.b From frontend to backend

There are two ways to send values from a html page to the Javalin backend:

  1. As query parameters (GET link)

In the html page:

<a href="/users?usergroup=1&page=2">view users</a>

The two query parameters usergroup and page is sent to the backend route /users. When arrived you can retrieve the parameters like this:

int usergroup = Integer.parseInt(ctx.queryParam("usergroup"));
int page = Integer.parseInt(ctx.queryParam("page"));

Note: In an ideal world we should wrap the conversions in a try-catch block, since ctx.queryParam("usergroup") and ctx.queryParam("page") return a String - hopefully with a value.

  1. As form parameters (GET or POST )

In the html page:

<form method="post">
    <input type="text" name="username" placeholder="username">
    <input type="password" name="password" placeholder="password">
    <button formaction="/login" type="submit" value="login">Login</button>
</form>

The two form parameters username and password are sent along the http POST request to Javalin and lands at the route “/login”. Then the values can be retrieved like this:

String userName = ctx.formParam("username");
String password = ctx.formParam("password");

Note: it is easy to spell the name attribute wrong in the form element. In the <input type="text" name="username" placeholder="username"> element name="username" is the form parameter that we expect on the Javalin side.


Top

2. semester forår 2025