CtrlK
BlogDocsLog inGet started
Tessl Logo

quarkus-project-starter

Invalid
This skill can't be scored yet
Validation errors are blocking scoring. Review and fix them to unlock Quality, Impact and Security scores. See what needs fixing →
SKILL.md
Quality
Evals
Security

Quarkus Project Starter

Scaffold a cloud-native Quarkus 3.x application with Java 21+, RESTEasy Reactive, Hibernate Reactive with Panache, Dev Services, and GraalVM native-image support.

Prerequisites

  • Java 21+ (Eclipse Temurin or GraalVM)
  • Maven 3.9+ or Gradle 8.5+
  • Quarkus CLI (optional but recommended): curl -Ls https://sh.quarkus.io | bash
  • Docker (required for Dev Services and native-image container builds)
  • GraalVM 21+ with native-image (only for native compilation; not needed for JVM mode)

Scaffold Command

# 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

Project Structure

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

Key Conventions

  • Quarkus uses CDI (Contexts and Dependency Injection) for all dependency injection. Prefer @ApplicationScoped for services.
  • Use Panache Active Record pattern (entity extends PanacheEntity) for simple CRUD, or Panache Repository pattern for more complex cases.
  • RESTEasy Reactive endpoints return Uni<T> or Multi<T> for reactive, or plain types for blocking (annotate blocking methods with @Blocking).
  • Use Java records for DTOs. Quarkus with Jackson handles records out of the box.
  • Dev Services auto-provision PostgreSQL, Kafka, Redis, etc. in dev/test mode. No manual Docker Compose needed for development.
  • Configuration via application.properties. Use %dev., %test., %prod. profile prefixes inline.
  • Flyway requires the jdbc-postgresql extension alongside reactive-pg-client because Flyway uses JDBC.
  • Prefer field injection with @Inject in Quarkus (CDI-idiomatic), or constructor injection. Both are fine.
  • REST resources (controllers) are JAX-RS annotated classes, not Spring @RestController.

Essential Patterns

Panache Entity (Active Record)

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();
    }
}

DTOs (Java Records)

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
) {}

Service with CDI

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);
    }
}

RESTEasy Reactive Resource

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());
    }
}

Exception Mapper

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) {}
}

Qute Template (server-side HTML)

<!-- 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

Flyway Migration

-- 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
);

application.properties

# --- 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

REST Assured Test

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": "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);
    }
}

First Steps After Scaffold

  1. Ensure Docker is running (Quarkus Dev Services auto-provisions PostgreSQL in dev mode)
  2. Run ./mvnw compile to verify the project compiles
  3. Start dev mode: quarkus dev (or ./mvnw quarkus:dev) -- Dev Services starts a PostgreSQL container automatically and Flyway runs migrations
  4. Verify the health endpoint: curl http://localhost:8080/q/health
  5. Open the Swagger UI: http://localhost:8080/q/swagger-ui

Common Commands

# 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

Integration Notes

  • Dev Services: In dev and test modes, Quarkus auto-starts containers for PostgreSQL, Kafka, Redis, Keycloak, and more. No Docker Compose needed for local development. Just add the client extension and Quarkus handles the rest.
  • Reactive vs. Blocking: RESTEasy Reactive handles both. Reactive methods return Uni<T>/Multi<T>. For blocking operations (JDBC, file I/O), annotate with @Blocking or use the JDBC datasource alongside the reactive one.
  • Flyway + Reactive: Flyway requires a JDBC datasource. Add both jdbc-postgresql and reactive-pg-client extensions. Flyway uses JDBC for migrations; the application uses the reactive client at runtime.
  • Native Image: Not all libraries are GraalVM-compatible. Quarkus extensions handle reflection registration automatically. For custom classes needing reflection, use @RegisterForReflection.
  • Frontend: Quarkus serves static files from src/main/resources/META-INF/resources/. For SPA frameworks, place the built output there. Qute templates work well for server-rendered pages.
  • API Documentation: The smallrye-openapi extension auto-generates OpenAPI specs. Swagger UI is available at /q/swagger-ui in dev mode.
  • Observability: Add quarkus-micrometer-registry-prometheus for Prometheus metrics. Health checks are at /q/health via smallrye-health.
  • Security: Add quarkus-oidc for OpenID Connect or quarkus-smallrye-jwt for JWT-based auth. Both integrate with the CDI @RolesAllowed annotations.
Repository
achreftlili/deep-dev-skills
Last updated
First committed

Is this your skill?

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.