Scaffold a cloud-native Quarkus 3.x application with Java 21+, RESTEasy Reactive, Hibernate Reactive with Panache, Dev Services, and GraalVM native-image support.
curl -Ls https://sh.quarkus.io | bashnative-image (only for native compilation; not needed for JVM mode)# Via Quarkus CLI
quarkus create app com.example:myapp \
--java=21 \
--extension='resteasy-reactive-jackson,hibernate-reactive-panache,reactive-pg-client,smallrye-openapi,smallrye-health,hibernate-validator,flyway,jdbc-postgresql,qute' \
--no-code
# Via Maven directly
mvn io.quarkus.platform:quarkus-maven-plugin:3.17.5:create \
-DprojectGroupId=com.example \
-DprojectArtifactId=myapp \
-Dextensions='resteasy-reactive-jackson,hibernate-reactive-panache,reactive-pg-client,smallrye-openapi,smallrye-health,hibernate-validator,flyway,jdbc-postgresql,qute'
cd myappmyapp/
├── src/
│ ├── main/
│ │ ├── java/com/example/myapp/
│ │ │ ├── entity/
│ │ │ │ └── User.java
│ │ │ ├── resource/
│ │ │ │ └── UserResource.java
│ │ │ ├── service/
│ │ │ │ └── UserService.java
│ │ │ ├── dto/
│ │ │ │ ├── UserRequest.java
│ │ │ │ └── UserResponse.java
│ │ │ └── exception/
│ │ │ └── ErrorMapper.java
│ │ ├── resources/
│ │ │ ├── application.properties
│ │ │ ├── db/migration/
│ │ │ │ └── V1__create_users_table.sql
│ │ │ └── templates/
│ │ │ └── user.html
│ │ └── docker/
│ │ ├── Dockerfile.jvm
│ │ └── Dockerfile.native
│ └── test/
│ └── java/com/example/myapp/
│ ├── resource/
│ │ └── UserResourceTest.java
│ └── entity/
│ └── UserTest.java
├── pom.xml
└── .dockerignore@ApplicationScoped for services.PanacheEntity) for simple CRUD, or Panache Repository pattern for more complex cases.Uni<T> or Multi<T> for reactive, or plain types for blocking (annotate blocking methods with @Blocking).application.properties. Use %dev., %test., %prod. profile prefixes inline.jdbc-postgresql extension alongside reactive-pg-client because Flyway uses JDBC.@Inject in Quarkus (CDI-idiomatic), or constructor injection. Both are fine.@RestController.package com.example.myapp.entity;
import io.quarkus.hibernate.reactive.panache.PanacheEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import java.time.Instant;
@Entity
@Table(name = "users")
public class User extends PanacheEntity {
@Column(nullable = false, unique = true)
public String email;
@Column(nullable = false)
public String name;
@Column(name = "created_at", nullable = false, updatable = false)
public Instant createdAt;
@Column(name = "updated_at")
public Instant updatedAt;
// Panache uses public fields by default (no getters/setters needed)
// Custom query methods are static
public static Uni<User> findByEmail(String email) {
return find("email", email).firstResult();
}
}package com.example.myapp.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
public record UserRequest(
@NotBlank(message = "Name is required")
@Size(min = 2, max = 100)
String name,
@NotBlank(message = "Email is required")
@Email(message = "Email must be valid")
String email
) {}package com.example.myapp.dto;
import java.time.Instant;
public record UserResponse(
Long id,
String name,
String email,
Instant createdAt
) {}package com.example.myapp.service;
import com.example.myapp.dto.UserRequest;
import com.example.myapp.dto.UserResponse;
import com.example.myapp.entity.User;
import io.quarkus.hibernate.reactive.panache.common.WithTransaction;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import java.time.Instant;
import java.util.List;
@ApplicationScoped
public class UserService {
public Uni<List<UserResponse>> findAll() {
return User.<User>listAll()
.map(users -> users.stream()
.map(this::toResponse)
.toList());
}
public Uni<UserResponse> findById(Long id) {
return User.<User>findById(id)
.onItem().ifNull().failWith(() ->
new NotFoundException("User not found: " + id))
.map(this::toResponse);
}
@WithTransaction
public Uni<UserResponse> create(UserRequest request) {
User user = new User();
user.name = request.name();
user.email = request.email();
user.createdAt = Instant.now();
return user.<User>persist().map(this::toResponse);
}
@WithTransaction
public Uni<UserResponse> update(Long id, UserRequest request) {
return User.<User>findById(id)
.onItem().ifNull().failWith(() ->
new NotFoundException("User not found: " + id))
.invoke(user -> {
user.name = request.name();
user.email = request.email();
user.updatedAt = Instant.now();
})
.map(this::toResponse);
}
@WithTransaction
public Uni<Void> delete(Long id) {
return User.deleteById(id)
.invoke(deleted -> {
if (!deleted) throw new NotFoundException("User not found: " + id);
})
.replaceWithVoid();
}
private UserResponse toResponse(User user) {
return new UserResponse(user.id, user.name, user.email, user.createdAt);
}
}package com.example.myapp.resource;
import com.example.myapp.dto.UserRequest;
import com.example.myapp.dto.UserResponse;
import com.example.myapp.service.UserService;
import io.smallrye.mutiny.Uni;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.net.URI;
import java.util.List;
@Path("/api/v1/users")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class UserResource {
@Inject
UserService userService;
@GET
public Uni<List<UserResponse>> findAll() {
return userService.findAll();
}
@GET
@Path("/{id}")
public Uni<UserResponse> findById(@PathParam("id") Long id) {
return userService.findById(id);
}
@POST
public Uni<Response> create(@Valid UserRequest request) {
return userService.create(request)
.map(user -> Response
.created(URI.create("/api/v1/users/" + user.id()))
.entity(user)
.build());
}
@PUT
@Path("/{id}")
public Uni<UserResponse> update(@PathParam("id") Long id, @Valid UserRequest request) {
return userService.update(id, request);
}
@DELETE
@Path("/{id}")
public Uni<Response> delete(@PathParam("id") Long id) {
return userService.delete(id)
.map(v -> Response.noContent().build());
}
}package com.example.myapp.exception;
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;
@Provider
public class ErrorMapper implements ExceptionMapper<Exception> {
@Override
public Response toResponse(Exception exception) {
int status = 500;
String message = "Internal Server Error";
if (exception instanceof NotFoundException) {
status = 404;
message = exception.getMessage();
} else if (exception instanceof IllegalArgumentException) {
status = 400;
message = exception.getMessage();
}
return Response.status(status)
.entity(new ErrorResponse(status, message))
.build();
}
public record ErrorResponse(int status, String message) {}
}<!-- src/main/resources/templates/user.html -->
{@com.example.myapp.dto.UserResponse user}
<!DOCTYPE html>
<html>
<head><title>{user.name}</title></head>
<body>
<h1>{user.name}</h1>
<p>Email: {user.email}</p>
<p>Joined: {user.createdAt}</p>
</body>
</html>// Template injection in a resource
@Path("/users/{id}/page")
@GET
@Produces(MediaType.TEXT_HTML)
public Uni<String> userPage(@PathParam("id") Long id) {
return userService.findById(id)
.map(user -> userTemplate.data("user", user).render());
}
@Inject
Template user; // matches templates/user.html by field name-- src/main/resources/db/migration/V1__create_users_table.sql
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
name VARCHAR(100) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ
);# --- Default (shared) ---
quarkus.application.name=myapp
quarkus.http.port=8080
# Flyway (uses JDBC datasource)
quarkus.flyway.migrate-at-start=true
# Hibernate
quarkus.hibernate-orm.database.generation=validate
quarkus.hibernate-orm.log.sql=false
# Health / OpenAPI
quarkus.smallrye-openapi.path=/q/openapi
quarkus.swagger-ui.always-include=true
# --- Dev profile (auto-applied in quarkus:dev) ---
# %dev. prefix = only active in dev profile. Use %prod. for production, %test. for tests.
%dev.quarkus.hibernate-orm.log.sql=true
%dev.quarkus.log.category."com.example".level=DEBUG
# Dev Services auto-provisions PostgreSQL in dev and test modes.
# No datasource URL needed — Quarkus detects the reactive-pg-client
# extension and starts a container automatically.
# --- Prod profile ---
%prod.quarkus.datasource.db-kind=postgresql
%prod.quarkus.datasource.username=${DATABASE_USERNAME}
%prod.quarkus.datasource.password=${DATABASE_PASSWORD}
%prod.quarkus.datasource.reactive.url=postgresql://${DATABASE_HOST}:5432/${DATABASE_NAME}
%prod.quarkus.datasource.jdbc.url=jdbc:postgresql://${DATABASE_HOST}:5432/${DATABASE_NAME}
%prod.quarkus.log.category."com.example".level=INFOpackage com.example.myapp.resource;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.http.ContentType;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;
@QuarkusTest
class UserResourceTest {
@Test
void createUser_returns201() {
given()
.contentType(ContentType.JSON)
.body("""
{"name": "Alice", "email": "alice@example.com"}
""")
.when()
.post("/api/v1/users")
.then()
.statusCode(201)
.body("name", is("Alice"))
.body("id", notNullValue());
}
@Test
void findAll_returnsOk() {
given()
.when()
.get("/api/v1/users")
.then()
.statusCode(200)
.body("$", is(instanceOf(java.util.List.class)));
}
@Test
void createUser_invalidEmail_returns400() {
given()
.contentType(ContentType.JSON)
.body("""
{"name": "Bob", "email": "not-an-email"}
""")
.when()
.post("/api/v1/users")
.then()
.statusCode(400);
}
}./mvnw compile to verify the project compilesquarkus dev (or ./mvnw quarkus:dev) -- Dev Services starts a PostgreSQL container automatically and Flyway runs migrationscurl http://localhost:8080/q/healthhttp://localhost:8080/q/swagger-ui# Start in dev mode (live reload + Dev Services)
quarkus dev
# or: ./mvnw quarkus:dev
# Run tests (Dev Services auto-provisions test DB)
./mvnw test
# Run a single test class
./mvnw test -Dtest=UserResourceTest
# List available extensions
quarkus extension list
# Add an extension
quarkus extension add smallrye-jwt
# Package as JVM JAR (fast-jar format)
./mvnw clean package
# Run the packaged JVM JAR
java -jar target/quarkus-app/quarkus-run.jar
# Build native executable (requires GraalVM or uses container build)
./mvnw clean package -Dnative
# Build native executable using a container (no local GraalVM needed)
./mvnw clean package -Dnative -Dquarkus.native.container-build=true
# Run the native executable
./target/myapp-1.0.0-SNAPSHOT-runner
# Build a container image
./mvnw clean package -Dquarkus.container-image.build=trueUni<T>/Multi<T>. For blocking operations (JDBC, file I/O), annotate with @Blocking or use the JDBC datasource alongside the reactive one.jdbc-postgresql and reactive-pg-client extensions. Flyway uses JDBC for migrations; the application uses the reactive client at runtime.@RegisterForReflection.src/main/resources/META-INF/resources/. For SPA frameworks, place the built output there. Qute templates work well for server-rendered pages.smallrye-openapi extension auto-generates OpenAPI specs. Swagger UI is available at /q/swagger-ui in dev mode.quarkus-micrometer-registry-prometheus for Prometheus metrics. Health checks are at /q/health via smallrye-health.quarkus-oidc for OpenID Connect or quarkus-smallrye-jwt for JWT-based auth. Both integrate with the CDI @RolesAllowed annotations.181fcbc
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.