Todo App (Javalin)

A working todo list backed by a SQLite database: add todos, check them off, delete them, and page through them. It is three short files, with no separate data-access layer to write.

You write the table as a migration, the row as a Todo bean, and four routes wired to Javalin. Because the bean persists and validates itself, the route handlers stay a few lines each.

Add Javalin and a JDBC driver alongside PikaORM (see Get Started):

<dependency>
    <groupId>io.javalin</groupId>
    <artifactId>javalin</artifactId>
    <version>6.7.0</version>
</dependency>

1. The schema

public class TodoMigrations extends Migrations {
    @Override
    public void migrations() {
        add(this::createTodos);
    }

    public PikaMigration createTodos() {
        return makeMigration("001_create_todos")
                .up("""
                    CREATE TABLE IF NOT EXISTS todos (
                        id          INTEGER PRIMARY KEY,
                        title       TEXT,
                        description TEXT,
                        completed   INTEGER
                    );""")
                .down("DROP TABLE todos;");
    }
}

2. The bean

import edu.montana.pika.bean.PikaBean;
import edu.montana.pika.query.PikaClassFinder;

public class Todo extends PikaBean {
    Long id;
    String title;
    String description;
    Boolean completed = false;

    public Todo() {}

    public Long getId()            { return id; }
    public String getTitle()       { return title; }
    public Boolean getCompleted()  { return completed; }
    public void setCompleted(Boolean c) { this.completed = c; }

    @Override
    protected void validation() {
        require("title");
    }

    public static PikaClassFinder<Todo> find() {
        return find(Todo.class);
    }
}

3. The app

import edu.montana.pika.PikaORM;
import edu.montana.pika.query.PikaClassQuery;
import io.javalin.Javalin;
import io.javalin.http.Context;

public class TodoApp {

    static final int PAGE_SIZE = 10;

    public static void main(String[] args) {
        new PikaORM("jdbc:sqlite:todo.db")
                .makeDefaultORM()
                .withMigrations(new TodoMigrations())
                .applyMigrations();

        Javalin.create()
                .get("/", TodoApp::list)
                .post("/todos", TodoApp::create)
                .post("/todos/{id}/toggle", TodoApp::toggle)
                .post("/todos/{id}/delete", TodoApp::delete)
                .start(7070);
    }

    // GET / -- one page of todos
    static void list(Context ctx) {
        long page = ctx.queryParamAsClass("page", Long.class).getOrDefault(1L);

        PikaClassQuery<Todo> query = Todo.find()
                .orderBy("id")
                .page(page)
                .pageSize(PAGE_SIZE);

        ctx.html(render(query.fetchList(), page, query.hasPreviousPage(), query.hasNextPage()));
    }

    // POST /todos -- create from the form
    static void create(Context ctx) {
        Todo todo = new Todo();
        todo.setFieldsFrom(ctx::formParam, "title", "description");  // only these fields
        todo.save();                                                 // validates, then INSERTs
        ctx.redirect("/");
    }

    // POST /todos/{id}/toggle -- flip completed
    static void toggle(Context ctx) {
        Todo todo = Todo.find().byId(Long.parseLong(ctx.pathParam("id")));
        if (todo != null) {
            todo.setCompleted(!Boolean.TRUE.equals(todo.getCompleted()));
            todo.save();
        }
        ctx.redirect("/");
    }

    // POST /todos/{id}/delete
    static void delete(Context ctx) {
        Todo todo = Todo.find().byId(Long.parseLong(ctx.pathParam("id")));
        if (todo != null) todo.delete();
        ctx.redirect("/");
    }

    // Minimal HTML. A real app would escape user input and use a template.
    static String render(java.util.List<Todo> todos, long page, boolean prev, boolean next) {
        StringBuilder sb = new StringBuilder("""
                <form method="post" action="/todos">
                  <input name="title" placeholder="What needs doing?">
                  <button>Add</button>
                </form>
                <ul>
                """);
        for (Todo t : todos) {
            sb.append("<li>")
              .append("<form method=post action=/todos/").append(t.getId()).append("/toggle>")
              .append("<button>").append(Boolean.TRUE.equals(t.getCompleted()) ? "[x]" : "[ ]").append("</button></form> ")
              .append(t.getTitle())
              .append(" <form method=post action=/todos/").append(t.getId()).append("/delete><button>x</button></form>")
              .append("</li>");
        }
        sb.append("</ul><div>");
        if (prev) sb.append("<a href='/?page=").append(page - 1).append("'>prev</a> ");
        sb.append("page ").append(page);
        if (next) sb.append(" <a href='/?page=").append(page + 1).append("'>next</a>");
        return sb.append("</div>").toString();
    }
}

Run it and open http://localhost:7070.

See Paging for the paging API and Validation & Forms for setFieldsFrom.

Full version

The repo ships a larger TodoMVC build of this with search, filters, comments, and optimistic-concurrency conflict handling: