Generating Jakarta REST resources with Panache
A lot of web applications are monotonous CRUD applications with REST APIs that are tedious to write. To streamline this task, REST Data with Panache extension can generate the basic CRUD endpoints for your entities and repositories.
Currently, this extension supports Hibernate ORM and MongoDB with Panache and can generate CRUD resources that work with application/json and application/hal+json content and generates REST Resources backed by Quarkus REST.
Setting up REST Data with Panache
Quarkus provides the following extensions to set up REST Data with Panache. Please, check out the next compatibility table to use the right one according to the technology you are using:
| Extension | Status | Hibernate |
|---|---|---|
|
|
|
|
|
|
|
|
Hibernate ORM
-
Add the required dependencies to your build file
-
Hibernate ORM REST Data with Panache extension (
quarkus-hibernate-orm-rest-data-panache) -
A JDBC driver extension (
quarkus-jdbc-postgresql,quarkus-jdbc-h2,quarkus-jdbc-mariadb, …) -
One of the REST JSON serialization extensions (such as
quarkus-rest-jackson)
-
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-orm-rest-data-panache</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jdbc-postgresql</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-jackson</artifactId>
</dependency>
-->
implementation("io.quarkus:quarkus-hibernate-orm-rest-data-panache")
implementation("io.quarkus:quarkus-jdbc-postgresql")
implementation("io.quarkus:quarkus-rest-jackson")
-
Implement the Panache entities and/or repositories as explained in the Hibernate ORM with Panache guide.
-
Define the interfaces for generation as explained in the Generating resources section.
To see the Hibernate ORM REST Data with Panache in action, you can check out the hibernate-orm-rest-data-panache-quickstart quickstart.
Hibernate Reactive
-
Add the required dependencies to your
pom.xml-
Hibernate Reactive REST Data with Panache extension (
quarkus-hibernate-reactive-rest-data-panache) -
A Vert.x reactive database driver extension (
quarkus-reactive-pg-client,quarkus-reactive-mysql-client, …) -
One of the Quarkus REST serialization extensions (
quarkus-rest-jackson,quarkus-rest-jsonb, …)
-
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-reactive-rest-data-panache</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-reactive-pg-client</artifactId>
</dependency>
<!-- Use this if you are using REST Jackson for serialization -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-jackson</artifactId>
</dependency>
</dependencies>
-
Implement the Panache entities and/or repositories as explained in the Hibernate Reactive with Panache guide.
-
Define the interfaces for generation as explained in the resource generation section.
MongoDB
-
Add the required dependencies to your build file
-
MongoDB REST Data with Panache extension (
quarkus-mongodb-rest-data-panache) -
One of the RESTEasy JSON serialization extensions (
quarkus-rest-jacksonorquarkus-rest-jsonb)
-
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-mongodb-rest-data-panache</artifactId>
</dependency>
<!-- Use this if you are using Quarkus REST -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-jackson</artifactId>
</dependency>
implementation("io.quarkus:quarkus-mongodb-rest-data-panache")
// Use this if you are using Quarkus REST
implementation("io.quarkus:quarkus-rest-jackson")
-
Implement the Panache entities and/or repositories as explained in the MongoDB with Panache guide.
-
Define the interfaces for generation as explained in the resource generation section.
Generating resources
REST Data with Panache generates Jakarta REST resources based on the interfaces available in your application. For each entity and repository that you want to generate, provide a resource interface. Do not implement these interfaces and don’t provide custom methods because they will be ignored. You can, however, override the methods from the extended interface in order to customize them (see the section at the end).
PanacheEntityResource
If your application has an entity (e.g. Person) that extends either PanacheEntity or PanacheEntityBase class, you could instruct REST Data with Panache to generate its Jakarta REST resource with the following interface:
public interface PeopleResource extends PanacheEntityResource<Person, Long> {
}
PanacheRepositoryResource
If your application has a simple entity (e.g. Person) and a repository (e.g. PersonRepository) that implements either PanacheRepository or PanacheRepositoryBase interface, you could instruct REST Data with Panache to generate its Jakarta REST resource with the following interface:
public interface PeopleResource extends PanacheRepositoryResource<PersonRepository, Person, Long> {
}
PanacheMongoEntityResource
If your application has an entity (e.g. Person) that extends either PanacheMongoEntity or PanacheMongoEntityBase class, you could instruct REST Data with Panache to generate its Jakarta REST resource with the following interface:
public interface PeopleResource extends PanacheMongoEntityResource<Person, Long> {
}
PanacheMongoRepositoryResource
If your application has a simple entity (e.g. Person) and a repository (e.g. PersonRepository) that implements either PanacheMongoRepository or PanacheMongoRepositoryBase interface, you could instruct REST Data with Panache to generate its Jakarta REST resource with the following interface:
public interface PeopleResource extends PanacheMongoRepositoryResource<PersonRepository, Person, Long> {
}
The generated resource
The generated resources will be functionally equivalent for both entities and repositories. The only difference being the particular data access pattern and data storage in use.
If you have defined one of the PeopleResource interfaces mentioned above, this extension will generate its implementation using a particular data access strategy.
The implemented class then will be used by a generated Jakarta REST resource, which will look like this:
public class PeopleResourceJaxRs { // The actual class name is going to be unique
@Inject
PeopleResource resource;
@GET
@Path("{id}")
@Produces("application/json")
public RestResponse<Person> get(@PathParam("id") Long id){
Person person = resource.get(id);
if (person == null) {
return ResponseBuilder.create(404).build();
} else {
return ResponseBuilder.ok(person).build();
}
}
@GET
@Produces("application/json")
public RestResponse<Person> list(@QueryParam("sort") List<String> sortQuery,
@QueryParam("page") @DefaultValue("0") int pageIndex,
@QueryParam("size") @DefaultValue("20") int pageSize) {
Page page = Page.of(pageIndex, pageSize);
Sort sort = getSortFromQuery(sortQuery);
List<Person> people = resource.list(page, sort);
// ... build a response with page links and return a 200 response with a list
}
@GET
@Path("/count")
public RestResponse<Long> count() {
return ResponseBuilder.ok(resource.count()).build();
}
@Transactional
@POST
@Consumes("application/json")
@Produces("application/json")
public RestResponse<Person> add(Person personToSave) {
Person person = resource.add(personToSave);
// ... build a new location URL and return 201 response with an entity
}
@Transactional
@PUT
@Path("{id}")
@Consumes("application/json")
@Produces("application/json")
public RestResponse<Person> update(@PathParam("id") Long id, Person personToSave) {
if (resource.get(id) == null) {
Person person = resource.update(id, personToSave);
return ResponseBuilder.create(204).build();
}
Person person = resource.update(id, personToSave);
// ... build a new location URL and return 201 response with an entity
}
@Transactional
@DELETE
@Path("{id}")
public RestResponse<Person> delete(@PathParam("id") Long id) {
return !resource.delete(id) ? ResponseBuilder.create(404).build() : ResponseBuilder.create(204).build();
}
}
Resource customisation
REST Data with Panache provides a @ResourceProperties and @MethodProperties annotations that can be used to customize certain features of the resource.
It can be used in your resource interface:
@ResourceProperties(hal = true, path = "my-people")
public interface PeopleResource extends PanacheEntityResource<Person, Long> {
@MethodProperties(path = "all")
List<Person> list(Page page, Sort sort);
@MethodProperties(exposed = false)
boolean delete(Long id);
}
Available options
@ResourceProperties
-
exposed- whether resource could be exposed. A global resource property that can be overridden for each method. Default istrue. -
path- resource base path. Default path is a hyphenated lowercase resource name without a suffix ofresourceorcontroller. -
rolesAllowed- List of the security roles permitted to access the resources. It needs a Quarkus Security extension to be present, otherwise it will be ignored. Default is empty. -
paged- whether collection responses should be paged or not. First, last, previous and next page URIs are included in the response headers if they exist. Request page index and size are taken from thepageandsizequery parameters that default to0and20respectively. Default istrue. -
hal- in addition to the standardapplication/jsonresponses, generates additional methods that can returnapplication/hal+jsonresponses if requested via anAcceptheader. Default isfalse. -
halCollectionName- name that should be used when generating a hal collection response. Default name is a hyphenated lowercase resource name without a suffix ofresourceorcontroller.
@MethodProperties
-
exposed- does not expose a particular HTTP verb when set tofalse. Default istrue. -
path- operation path (this is appended to the resource base path). Default is an empty string. -
rolesAllowed- List of the security roles permitted to access this operation. It needs a Quarkus Security extension to be present, otherwise it will be ignored. When left empty, the operation inherits therolesAlloweddeclared on the@ResourcePropertiesof the resource; set it to override the resource-level roles for this operation only. Default is empty.
Adding additional methods to the generated resource
You can add additional methods to the generated resources by the REST Data with Panache extension by adding these methods to the resource interface, for example:
@ResourceProperties
public interface PeopleResource extends PanacheEntityResource<Person, Long> {
@GET
@Path("/name/{name}")
@Produces("application/json")
default List<Person> findByName(@PathParam("name") String name) {
return Person.find("name = :name", Collections.singletonMap("name", name)).list();
}
}
And this method will be exposed along with the generated methods using http://localhost:8080/people/name/Johan.
Securing endpoints
REST Data with Panache will use the Security annotations within the package jakarta.annotation.security that are defined on your resource interfaces:
import jakarta.annotation.security.DenyAll;
import jakarta.annotation.security.RolesAllowed;
@DenyAll
@ResourceProperties
public interface PeopleResource extends PanacheEntityResource<Person, Long> {
@RolesAllowed("superuser")
boolean delete(Long id);
}
Additionally, if you are only interested in specifying the roles that are allowed to use the resources, the @ResourceProperties and @MethodProperties annotations have the field rolesAllowed to list the security roles permitted to access the resource or operation.
An operation that is customized with @MethodProperties but does not set its own rolesAllowed still inherits the rolesAllowed declared on the resource’s @ResourceProperties. If you instead want a single operation of an otherwise-secured resource to be publicly accessible, annotate that operation with jakarta.annotation.security.PermitAll (or with @Authenticated if you only want to require an authenticated user); a security annotation declared directly on the operation always takes precedence over the resource-level rolesAllowed.
Using @Authenticated
Additionally, REST Data with Panache can be used in conjunction with io.quarkus.security.Authenticated, by either:
-
Setting
@ResourceProperties(authenticated=true)which results in all methods of the resource requiring authentication -
The same result can also be achieved by adding the
@Authenticatedannotation on the interface instead of using theauthenticatedproperty -
Finally, the
@Authenticatedannotation can be used on specific methods, meaning that only those will require an authenticated user. An example might look like so:
@ResourceProperties
public interface PeopleResource extends PanacheEntityResource<Person, Long> {
@Authenticated
boolean delete(Long id);
}
As with rolesAllowed, a security annotation placed directly on an operation takes precedence over @ResourceProperties(authenticated=true): the operation is governed by its own annotation only. For example, annotating a single operation with jakarta.annotation.security.PermitAll makes that operation publicly accessible while every other operation of the resource still requires an authenticated user.
Securing endpoints using @PermissionsAllowed annotation
You can also secure your endpoints with the @PermissionsAllowed security annotation like in the example below:
package org.acme.security;
import io.quarkus.rest.data.panache.ResourceProperties;
import io.quarkus.security.PermissionsAllowed;
@ResourceProperties
public interface PeopleResource extends PanacheEntityResource<Person, Long> {
@PermissionsAllowed("delete")
boolean delete(Long id);
}
Securing sort and filter fields
REST Data with Panache validates the sort query parameter and field filter parameters against the entity’s actual fields.
This prevents users from attempting to sort or filter by non-existent fields, which could potentially be used for enumeration attacks or cause database errors.
Additionally, fields that are hidden from JSON serialization are automatically excluded from sorting and filtering:
-
Fields annotated with
@JsonIgnore(Jackson) -
Fields annotated with
@JsonbTransient(JSON-B) -
Fields annotated with
@XmlTransient(JAXB) -
Fields listed in
@JsonIgnorePropertieson the entity class -
Fields whose type is annotated with
@JsonIgnoreType
Securing fields with @SecureField
When using Quarkus REST with Jackson, you can use the @SecureField annotation to restrict sorting and filtering by sensitive fields based on the user’s security roles.
If a field is annotated with @SecureField and the user lacks the required roles, attempting to sort or filter by that field will result in a 400 Bad Request response.
import io.quarkus.resteasy.reactive.jackson.SecureField;
@Entity
public class Employee extends PanacheEntity {
public String name;
public String department;
@SecureField(rolesAllowed = {"hr", "admin"})
public BigDecimal salary;
@SecureField(rolesAllowed = "admin")
public String ssn;
}
With this configuration:
-
Any user can sort or filter by
nameanddepartment -
Only users with the
hroradminrole can use?sort=salaryor?salary=50000 -
Only users with the
adminrole can use?sort=ssnor?ssn=123-45-6789 -
Unauthenticated users or users without the required roles will receive a
400 Bad Requestresponse that treats the field as invalid, to avoid revealing the existence of protected fields
The @SecureField annotation is provided by the quarkus-rest-jackson extension.
See Secure Serialization in the Quarkus REST guide for more details.
|
Query parameters to list entities
REST Data with Panache supports the following query parameters to get the list of entities:
-
page- a page number which should be returned by a list operation. It applies to the paged resources only and is a number starting with 0. Default is 0. -
size- a page size which should be returned by a list operation. It applies to the paged resources only and is a number starting with 1. Default is 20. The maximum page size is limited to 100 by default to prevent excessive database load. You can configure this limit using thequarkus.rest-data-panache.max-page-sizeproperty. Set it to empty to disable the limit entirely. -
sort- a comma separated list of fields which should be used for sorting a result of a list operation. Fields are sorted in the ascending order unless they’re prefixed with a-. E.g.?sort=name,-agewill sort the result by the name ascending by the age descending. Only direct entity fields are supported; nested properties (e.g.,author.name) cannot be used for sorting. -
namedQuery- a named query that should be configured at entity level using the annotation@NamedQuery.
These query parameter names (page, size, sort, namedQuery) are reserved by the framework.
If your entity has a field with one of these names, it cannot be filtered via query parameters.
|
For example, if you want to get two People entities in the first page, you should call http://localhost:8080/people?page=0&size=2, and the response should look like:
[
{
"id": 1,
"name": "John Johnson",
"birth": "1988-01-10"
},
{
"id": 2,
"name": "Peter Peterson",
"birth": "1986-11-20"
}
]
Additionally, you can also filter by the entity fields by adding a query param with the name of the field and the value, for example, calling http://localhost:8080/people?name=Peter Peterson would return:
[
{
"id": 2,
"name": "Peter Peterson",
"birth": "1986-11-20"
}
]
Filtering by fields is only supported for String, Boolean, Character, Double, Float, Integer, Long, Short, Byte and the primitive types.
Nested properties (e.g., author.name) are not supported for filtering.
|
Complex filtering to list entities using @NamedQuery
You can specify a named query to filter when listing the entities. For example, having the following named query in your entity:
@Entity
@NamedQuery(name = "Person.containsInName", query = "from Person where name like CONCAT('%', CONCAT(:name, '%'))")
public class Person extends PanacheEntity {
String name;
}
In this example, we have added a named query to list all the persons that contains some text in the name field.
Next, we can set a query param namedQuery when listing the entities using the generated resource with the name of the named query that we want to use, for example, calling http://localhost:8080/people?namedQuery=Person.containsInName&name=ter would return all the persons which name contains the text "ter".
For more information about how named queries work, go to the Hibernate ORM guide or to the Hibernate Reactive guide.
Resource Method Before/After Listeners
REST Data with Panache supports the subscription to the following resource method hooks:
-
Before/After add resource
-
Before/After update resource
-
Before/After delete resource
To register your resource method listener, you need to provide a bean that implements the interface RestDataResourceMethodListener, for example:
@ApplicationScoped
public class PeopleRestDataResourceMethodListener implements RestDataResourceMethodListener<Person> {
@Override
public void onBeforeAdd(Person person) {
System.out.println("Before Save Person: " + person.name);
}
}
Response body examples
As mentioned above REST Data with Panache supports the application/json and application/hal+json response content types.
Here are a couple of examples of how a response body would look like for the get and list operations assuming there are five Person records in a database.
GET /people/1
Accept: application/json
{
"id": 1,
"name": "John Johnson",
"birth": "1988-01-10"
}
Accept: application/hal+json
{
"id": 1,
"name": "John Johnson",
"birth": "1988-01-10",
"_links": {
"self": {
"href": "http://example.com/people/1"
},
"remove": {
"href": "http://example.com/people/1"
},
"update": {
"href": "http://example.com/people/1"
},
"add": {
"href": "http://example.com/people"
},
"list": {
"href": "http://example.com/people"
}
}
}
GET /people?page=0&size=2
Accept: application/json
[
{
"id": 1,
"name": "John Johnson",
"birth": "1988-01-10"
},
{
"id": 2,
"name": "Peter Peterson",
"birth": "1986-11-20"
}
]
Accept: application/hal+json
{
"_embedded": [
{
"id": 1,
"name": "John Johnson",
"birth": "1988-01-10",
"_links": {
"self": {
"href": "http://example.com/people/1"
},
"remove": {
"href": "http://example.com/people/1"
},
"update": {
"href": "http://example.com/people/1"
},
"add": {
"href": "http://example.com/people"
},
"list": {
"href": "http://example.com/people"
}
}
},
{
"id": 2,
"name": "Peter Peterson",
"birth": "1986-11-20",
"_links": {
"self": {
"href": "http://example.com/people/2"
},
"remove": {
"href": "http://example.com/people/2"
},
"update": {
"href": "http://example.com/people/2"
},
"add": {
"href": "http://example.com/people"
},
"list": {
"href": "http://example.com/people"
}
}
}
],
"_links": {
"add": {
"href": "http://example.com/people"
},
"list": {
"href": "http://example.com/people"
},
"first": {
"href": "http://example.com/people?page=0&size=2"
},
"last": {
"href": "http://example.com/people?page=2&size=2"
},
"next": {
"href": "http://example.com/people?page=1&size=2"
}
}
}
Both responses would also contain these headers:
-
Link: < http://example.com/people?page=0&size=2 >; rel="first"
-
Link: < http://example.com/people?page=2&size=2 >; rel="last"
-
Link: < http://example.com/people?page=1&size=2 >; rel="next"
A previous link header (and hal link) would not be included, because the previous page does not exist.
Include/Exclude Jakarta REST classes
Using Build time conditions
Quarkus enables the inclusion or exclusion of Jakarta REST Resources, Providers and Features directly thanks to build time conditions in the same that it does for CDI beans.
Thus, the REST Data with Panache interfaces can be annotated with profile conditions (@io.quarkus.arc.profile.IfBuildProfile or @io.quarkus.arc.profile.UnlessBuildProfile) and/or with property conditions (io.quarkus.arc.properties.IfBuildProperty or io.quarkus.arc.properties.UnlessBuildProperty) to indicate to Quarkus at build time under which conditions the generated Jakarta REST classes should be included.
In the following example, Quarkus will include the generated resource from the PeopleResource interface if and only if the build profile app1 has been enabled.
@IfBuildProfile("app1")
public interface PeopleResource extends PanacheEntityResource<Person, Long> {
}
Using a runtime property
| This option is only available when using the Quarkus REST Quarkus extension. |
Quarkus can also conditionally disable the generated Jakarta REST Resources based on the value of runtime properties using the @io.quarkus.resteasy.reactive.server.EndpointDisabled annotation.
In the following example, Quarkus will exclude the generated resource from the PeopleResource interface at runtime if the application has some.property configured to "disable".
@EndpointDisabled(name = "some.property", stringValue = "disable")
public interface PeopleResource extends PanacheEntityResource<Person, Long> {
}