Engineering

Creating a modern Java application: Part 2

Author headshot
By Allan Almazan

We continue where we left off in creating a new Quarkus CRUD application. We take a look at how Quarkus handles your data and how we can connect to our database.

Back to all articles
Creating a modern Java application: Part 2

In a previous post we took a brief look at Java's new features and got ourselves set up with the current Java LTS (version 17) and installed the Quarkus framework. We looked at what quarkus start app provides for us at the start and took a look at Quarkus's useful dev UI at http://localhost:8080.

What do we have now? Let's take a quick look again:

$ quarkus extension ls
# Can also be:
$ quarkus ext ls

Current Quarkus extensions installed:

✬ ArtifactId                                      Extension Name
✬ quarkus-resteasy-reactive                       RESTEasy Reactive

To get more information, append `--full` to your command line.

Our goals

Looking back at the previous post our goals are:

  1. Create a TODO REST web application that consumes and produces JSON.
  2. It should have a CRUD (create, read, update, delete) interface for TODO list items.
  3. It should be integrated with a datasource. In this case, a database.

We have the beginnings of the first step and two other steps to go. At the end of this post we should have satisfied all three requirements.

RESTEasy and Jackson

Let's set up a quick POJO (a plan Java object and endpoint to get started. We know that the URL http://localhost:8080/hello works and just outputs plain text. Let's look at the steps to output something more useful like an instance of a TODO item.

We'll start with a basic Java class representing our TODO item:

// example path: demo-todo/src/main/java/com/example/Todo.java
public class Todo {
    private Integer id;
    private String title;


    public Todo(Integer id, String title) {
        this.id = id;
        this.title = title;
    }

    public Integer getId() {
        return id;
    }

    public String getTitle() {
        return title;
    }
}

Now let's add in an endpoint that creates a Todo instance and responds with that instance. In our GreetingResource class that contains the /hello endpoint, we'll add another method:

// demo-todo/src/main/java/com/example/GreetingResource.java
@Path("/hello")
public class GreetingResource {
    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String hello() {
        return "Hello from RESTEasy Reactive";
    }

    @GET
    @Path("{id}")
    public Todo getTodo(Integer id) {
        // We can request this endpoint via `localhost:8080/hello/{id}`
        return new Todo(id, "Create a new TODO item");
    }
}

Save those changes. If our dev server is running via quarkus dev, wait a few seconds for it to reload. If not, re-run quarkus dev. What does that URL show us now?

curl -i http://localhost:8080/hello/5
HTTP/1.1 200 OK
content-length: 25
Content-Type: application/octet-stream

com.example.Todo@58ad8aa9

Hmm. That doesn't look right. The Content-Type is wrong and so is the output on the last line. Looks like we're missing the @Produces annotation. Let's add that in.

@Path("/hello")
public class GreetingResource {
    // ... same code as above, but with this modified
    @GET
    @Path("{id}")
    // Add this, and import `MediaType`.gg
    @Produces(MediaType.APPLICATION_JSON)
    public Todo getTodo(Integer id) {
        return new Todo(id, "Create a new TODO item");
    }
}

What do we get now?

$ curl -i http://localhost:8080/hello/5
HTTP/1.1 200 OK
content-length: 25
Content-Type: application/json;charset=UTF-8

com.example.Todo@78953cff

The Content-Type looks right now, but the response is still not giving is what we want. What's going on? We're missing a package that handles converting our Todo instance into its JSON representation. To do this, we will need to add the package quarkus-resteasy-reactive-jackson.

$ quarkus ext add quarkus-resteasy-reactive-jackson
[SUCCESS] ✅  Extension io.quarkus:quarkus-resteasy-reactive-jackson has been installed

OK, let’s use curl one more time.

curl -i http://localhost:8080/hello/5
HTTP/1.1 200 OK
content-length: 41
Content-Type: application/json;charset=UTF-8

{"id":5,"title":"Create a new TODO item"}

Much better. A small step, but we're getting somewhere. As you can see the io.quarkus:quarkus-resteasy-reactive-jackson package takes care of converting our TODO POJO instance into JSON. The underlying library being used in the package, as its name suggests, is the Jackson library (GitHub page).

What do these packages do anyway? quarkus-resteasy-reactive is a JAX-RS implementation written to be tightly integrated with Quarkus and its underlying layers. The implementation gives us a set of tools to let us define our REST service in a clear and concise way while also being performant and flexible.

If you're familiar with other frameworks such as Express, Ruby on Rails, Django and Flask, the example code so far should look similar and should be easy to infer what the annotations such as @GET and @Produces do. Generally speaking, JAX-RS can be thought of as the "controller" part of most MVC (model-view-controller) frameworks. One key difference with using a JAX-RS implementation compared to those other frameworks, is that JAX-RS is simply a specification. There are a number of different implementations of JAX-RS, but once you understand it, you essentially know how to use all the implementations.

With the addition of quarkus-resteasy-reactive-jackson, we can actually remove quarkus-resteasy-reactive from our project if consuming and producing JSON is all our project needs. This can be done with quarkus ext rm quarkus-resteasy-reactive.

This now satisfies our first requirement:

✅ Create a TODO REST web application that consumes and produces JSON

It also partially satisfies the second requirement: "It should have a CRUD (create, read, update, delete) interface for TODO list items." This can technically be done with an in-memory variable or some sort of file we manage as users interact with the application, but we're better than that. The missing piece for this is basically requirement three: a database integration.

Accessing data

For this project we will use the ORM (Object Relational Mapper) called Hibernate ORM, or just Hibernate. Hibernate ORM is an implementation of the JPA spec, and it will help us manage our data within the application. It allows us to interact with our database with minimal code. There are also ways to create and run your own SQL queries if you need that flexibility.

In addition, we will use Quarkus's Panache extension to help smooth out the rough edges when starting a new project. This is a key package that brings developing a web app in Quarkus more in line with the likes of Rails and Django by making database interaction simpler and less tedious to use.

Finally, along with all the above, we'll be using PostgreSQL as our database. Technically, since our ORM will be doing the heavy lifting, the choice of database doesn't matter for the purposes of this post. You can choose any database you'd like that has a Quarkus driver extension (list of supported databases).

Setting up our database

First we need the necessary extensions. If you have quarkus dev up and running, you can stop that for now (press q to quit), then add the following packages:

$ quarkus ext add \
  quarkus-hibernate-reactive-panache \
  quarkus-jdbc-postgresql
[SUCCESS] ✅  Extension io.quarkus:quarkus-hibernate-reactive-panache has been installed
[SUCCESS] ✅  Extension io.quarkus:quarkus-jdbc-postgresql has been installed

Before you start quarkus dev again: Note that Quarkus will, by default, automatically create a "zero-config" database via Docker if it detects an installed database driver -- one of the extensions installed above. Additionally, your development database will be destroyed and recreated every time the Quarkus dev server restarts, and this includes every time you save a .java file. To prevent this, open the application.properties file in your project and add the below. Your file will likely be empty if this the first time opening it:

# By default this is `drop-and-create`
# More info at: https://quarkus.io/guides/hibernate-orm#hibernate-configuration-properties
quarkus.hibernate-orm.database.generation = update

Additional detail on how to customize dev services behavior if you want to disable or change other options in the future. This isn't explicitly mentioned in Quarkus's guides, but I believe it's good to know before we restart quarkus dev. At this point we'll assume our database is up and running with no problems.

Our first Entity

Let's convert our previous Todo POJO into a Panache entity. For the sake of space, only the finished class is shown below:

// example path: demo-todo/src/main/java/com/example/Todo.java
// Add @Entity and extend from `PanacheEntity`
@Entity
public class Todo extends PanacheEntity {
    // Previously we had an Id property, but since we now extend `PanacheEntity`,
    // `Id` is inherited into this class. We have a

    // We also change our data's fields as public. These should be our database columns.
    // The `@Column` annotation also modifies how we this column is defined in the database.
    // In this case it will be `NOT NULL`.
    @Column(nullable = false)
    public String title;

    // Adding in more data to make a better Todo object
    @Column(nullable = false)
    public String description;

    @ColumnDefault("false")
    public Boolean isDone;

    public Todo() {
    }

    public Todo(String title, String description) {
        this.title = title;
    }

    // We previously had getters like `getTitle` here, but not anymore.
    // These are all created automatically with Panache.
}

And let's create a new file TodoResource.java to be the handler for a new /todos endpoint:

// demo-todo/src/main/java/com/example/TodoResource.java

// This will handle the `/todos` endpoint
@Path("todos")
@ApplicationScoped
// This entire class and its methods produces and/or consumes JSON
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class TodoResource {
    // GET `/todos/{id}`
    @GET
    @Path("{id}")
    public Todo getOne(Long id) {
        // Attempt to find a Todo item, or throw an exception.
        // `findByIdOptional` is another method provided by Panache.
        Optional<Todo> maybeTodo = Todo.findByIdOptional(id);
        return maybeTodo.orElseThrow(NotFoundException::new);
    }

    // Simply list out all the Todos when `GET`ing `/todos/`
    // This is obviously not a good idea when we have a massive number of
    // items. Ideally we would paginate here.
    @GET
    @Path("")
    public List<Todo> getList() {
        return Todo.listAll();
    }


    // POST `/todos` with JSON data
    // `Transactional` to wrap this entire database call in a database transaction.
    @Transactional
    @POST
    @Path("")
    public Todo create(Todo newTodo) {
        // We're taking in the `newTodo` data as is and saving it to the database.
        // At this point `newTodo` shouldn't have an `id` property yet.
        newTodo.persist();
        // Return the newly saved data. `newTodo` will now have an id from the database.
        return newTodo;
    }

    @Transactional
    @DELETE
    @Path("{id}")
    public boolean delete(Long id) {
        // We can delete two ways. Getting the instance first then deleting...
        // Optional<Todo> maybeTodo = Todo.findByIdOptional(id);
        // Todo t = maybeTodo.orElseThrow(NotFoundException::new);
        // t.delete();

        // Or simply attempting to delete by the id
        boolean wasDeleted = Todo.deleteById(id);
        return wasDeleted;
    }
}

We can now check our running dev server by POSTing data and attempting to retrieve it.

# We start off with no items
$ curl -i http://localhost:8080/todos
HTTP/1.1 200 OK
content-length: 2
Content-Type: application/json;charset=UTF-8

[]


# Lets create a few items (only one shown here)
$ curl -i -XPOST http://localhost:8080/todos \
  -H Content-Type:application/json \
  -d '{"title": "Todo title", "description": "The description", "isDone": false}'

HTTP/1.1 200 OK
content-length: 76
Content-Type: application/json;charset=UTF-8

{"id":1,"title":"Todo title","description":"The description","isDone":false}

# And check if we have them all in the database
$ curl -i http://localhost:8080/todos
HTTP/1.1 200 OK
content-length: 236
Content-Type: application/json;charset=UTF-8

[{"id":1,"title":"Todo title","description":"The description","isDone":false},{"id":2,"title":"Todo title2","description":"The description2","isDone":false},{"id":3,"title":"Todo title3","description":"The description3","isDone":false}]

Conclusion

With that demonstration, it looks like we've now satisfied all three of our requirements! We've barely touched the surface with what Quarkus and its extensions ecosystem provide, but hopefully this post has been helpful with getting a simple REST API up and running. Of course, we're missing the big topic of packaging and deployment, but that can be very complex depending on your needs. Quarkus has great guides for those topics. A general one here and another guide here for one the big features of Quarkus, native executables.

Here at Anvil engineering, we've been looking at the exciting possibilities and improvements provided by the next generation of Java versions and frameworks. We have examples available for using the Anvil API in Java for those looking to integrate PDF filling or e-signing capabilities into their own Java projects. We hope this post has helped you get started on your next Java/Quarkus application and hope to see you again on the next post. If you have any questions or comments, drop us a message at developers@useanvil.com.

Get a Document AI demo (from a real person)

Request a 30-minute demo and we'll be in touch soon. During the meeting our team will listen to your use case and suggest which Anvil products can help.
    Want to try Anvil first?
    Sign up for free or try it now with any document.
    Want to try Anvil first?
    Sign up for free or try it now with any document.