backend-java/quarkus-project-starter/SKILL.md
--- name: quarkus-project-starter description: > Scaffold a cloud-native Quarkus 3.x application with Java 21+, RESTEasy Reactive, Hibernate Reactive with Panache, Dev Services, and GraalVM native-image support. category: backend-java agent-type: coding compatibility: Java 21+, Maven 3.9+ or Gradle 8.5+, Quarkus CLI (optional but recommended): `curl -Ls https://sh.quarkus.io | bash`, Docker, GraalVM 21+ with `native-image` --- # Quarkus Project Starter > Scaffold a cloud-native Quarkus 3
npx skillsauth add achreftlili/deep-dev-skills backend-java/quarkus-project-starterInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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 myapp
myapp/
├── 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=INFO
package 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": "[email protected]"}
""")
.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=true
Uni<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.testing
Set up Vitest 2.x with TypeScript for unit and component testing using test/describe/it, vi.fn/vi.mock/vi.spyOn, component testing with Testing Library, coverage (v8/istanbul), workspace config, and snapshot testing.
testing
Set up pytest 8.x with Python for unit and integration testing using fixtures (scope, autouse, parametrize), async tests (pytest-asyncio), mocking (unittest.mock, pytest-mock), coverage (pytest-cov), conftest.py patterns, and markers.
testing
Set up Playwright 1.49+ with TypeScript for E2E testing using page object model, fixtures, test.describe/test blocks, assertions, selectors, network mocking, CI configuration, and trace viewer.
testing
Set up Jest 30+ with TypeScript for unit tests, integration tests, mocking (jest.fn, jest.mock, jest.spyOn), coverage configuration, custom matchers, snapshot testing, and setup/teardown patterns.