mirror of
https://github.com/github/awesome-copilot.git
synced 2026-07-21 21:04:13 +00:00
chore: publish from main
This commit is contained in:
@@ -0,0 +1,435 @@
|
||||
---
|
||||
name: java-helidon
|
||||
description: 'Get best practices for developing applications with Helidon 4 (SE and MP). Use when working with Helidon SE or Helidon MP, HttpService routing, Helidon DB Client, MicroProfile Config, Helidon Security, or Helidon testing in Java 21+ projects.'
|
||||
---
|
||||
|
||||
# Helidon Best Practices
|
||||
|
||||
Your goal is to help me write high-quality Helidon applications by following established best practices.
|
||||
|
||||
## Helidon 3 → 4 API changes
|
||||
|
||||
Helidon 4 renamed or resignatured APIs that appear widely in their Helidon 3 form.
|
||||
The left column does not compile on Helidon 4. Check generated code against this table
|
||||
before returning it.
|
||||
|
||||
| Do not use (Helidon 3) | Use (Helidon 4) |
|
||||
| ---------------------------------------------------------- | ------------------------------------------------------------------ |
|
||||
| `io.helidon.common.http.Http.Status` | `io.helidon.http.Status` |
|
||||
| `io.helidon.webserver.Service` | `io.helidon.webserver.http.HttpService` |
|
||||
| `Routing.Rules`, `update(Routing.Rules)` | `HttpRules`, `routing(HttpRules)` |
|
||||
| `request.path().param("id")` | `request.path().pathParameters().get("id")` |
|
||||
| `String s = column.as(String.class)` | `column.getString()` or `column.get(String.class)` |
|
||||
| `dbClient.execute(exec -> ...)` returning `Single`/`Multi` | `dbClient.execute()` returning `Optional<DbRow>` / `Stream<DbRow>` |
|
||||
| `javax.*` | `jakarta.*` |
|
||||
| `helidon-microprofile-tests-junit5` | `helidon-microprofile-testing-junit5` |
|
||||
|
||||
`Value.as(Class)` in Helidon 4 returns `OptionalValue<T>`, not `T`. This is the single
|
||||
most common Helidon 4 compile error in generated code.
|
||||
|
||||
## Project Setup & Structure
|
||||
|
||||
- **Programming Model:** Determine whether the project uses Helidon SE or Helidon MP before generating code. Do not mix the two programming models unless explicitly required.
|
||||
- **Java Version:** Use Java 21 or later for Helidon 4 applications.
|
||||
- **Build Tool:** Use Maven (`pom.xml`) or Gradle (`build.gradle`) for dependency management.
|
||||
- **Dependency Management:** Use the Helidon BOM or platform to keep Helidon module versions aligned.
|
||||
- **Package Structure:** Organize code by feature or domain, such as `com.example.app.order` and `com.example.app.customer`, rather than only by technical layer.
|
||||
|
||||
## Helidon SE
|
||||
|
||||
- **Explicit Composition:** Construct services and dependencies explicitly in the application bootstrap layer.
|
||||
- **Constructor Injection:** Pass required dependencies through constructors and declare dependency fields as `private final`.
|
||||
- **HTTP Services:** Group related routes in focused `HttpService` implementations.
|
||||
- **Business Logic:** Keep business logic outside route handlers.
|
||||
- **Virtual Threads:** Prefer straightforward blocking code with Helidon 4 virtual-thread-based request handling. Do not introduce reactive complexity without a clear reason. Helidon 4 is not reactive; do not generate `Single`, `Multi`, or `CompletionStage` chains.
|
||||
|
||||
## Helidon MP
|
||||
|
||||
- **Jakarta and MicroProfile:** Prefer standard Jakarta EE and Eclipse MicroProfile APIs when available.
|
||||
- **Dependency Injection:** Use CDI with constructor injection for required dependencies.
|
||||
- **Bean Scopes:** Use CDI scopes such as `@ApplicationScoped` and `@RequestScoped` intentionally.
|
||||
- **Normal-Scoped Beans:** Add a non-private no-argument constructor to normal-scoped beans that use constructor injection, so the CDI client proxy can be created portably.
|
||||
- **Business Logic:** Keep Jakarta REST resource classes thin and delegate business operations to service classes.
|
||||
- **Portability:** Prefer portable Jakarta and MicroProfile APIs over Helidon-specific APIs when portability is important.
|
||||
|
||||
## Configuration
|
||||
|
||||
- **Externalized Configuration:** Store non-secret configuration in `application.yaml` or `application.properties`.
|
||||
- **Helidon SE Configuration:** Use Helidon Config and pass configuration values or typed configuration objects to components.
|
||||
- **Helidon MP Configuration:** Use MicroProfile Config for injected application settings.
|
||||
- **Environment Overrides:** Use environment variables or deployment-specific configuration sources for environment-dependent values.
|
||||
- **Secrets Management:** Never hardcode credentials, API keys, tokens, or private certificates.
|
||||
|
||||
## Web Layer
|
||||
|
||||
- **DTOs:** Use dedicated request and response models. Do not expose persistence entities directly through APIs.
|
||||
- **Validation:** Validate path parameters, query parameters, headers, and request bodies before invoking business logic.
|
||||
- **Status Codes:** Return appropriate HTTP status codes for successful, invalid, unauthorized, forbidden, missing, and failed requests. On `PUT` and `DELETE`, return 404 when the target does not exist rather than succeeding unconditionally.
|
||||
- **Error Handling:** Use centralized error handling in Helidon SE and Jakarta REST `ExceptionMapper` implementations in Helidon MP.
|
||||
- **Sensitive Information:** Do not expose stack traces, database details, filesystem paths, or internal exception messages to clients.
|
||||
|
||||
### Helidon SE Example
|
||||
|
||||
Use an `HttpService` to register routes programmatically. Keep request handlers small and delegate business logic to a service.
|
||||
|
||||
```java
|
||||
import io.helidon.http.Status;
|
||||
import io.helidon.webserver.http.HttpRules;
|
||||
import io.helidon.webserver.http.HttpService;
|
||||
import io.helidon.webserver.http.ServerRequest;
|
||||
import io.helidon.webserver.http.ServerResponse;
|
||||
|
||||
public final class CustomerHttpService implements HttpService {
|
||||
|
||||
private final CustomerService customerService;
|
||||
|
||||
public CustomerHttpService(CustomerService customerService) {
|
||||
this.customerService = customerService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void routing(HttpRules rules) {
|
||||
rules.get("/{id}", this::findById);
|
||||
}
|
||||
|
||||
private void findById(ServerRequest request, ServerResponse response) {
|
||||
var id = request.path().pathParameters().get("id");
|
||||
|
||||
customerService.findById(id)
|
||||
.ifPresentOrElse(
|
||||
response::send,
|
||||
() -> response.status(Status.NOT_FOUND_404).send()
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Register the HTTP service when constructing the server:
|
||||
|
||||
```java
|
||||
import io.helidon.webserver.WebServer;
|
||||
|
||||
WebServer server = WebServer.builder()
|
||||
.routing(routing -> routing.register("/customers", customerHttpService))
|
||||
.build()
|
||||
.start();
|
||||
```
|
||||
|
||||
### Helidon MP Example
|
||||
|
||||
Use Jakarta REST annotations for endpoints and CDI for dependency injection.
|
||||
|
||||
```java
|
||||
import jakarta.enterprise.context.RequestScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.PathParam;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
|
||||
@Path("/customers")
|
||||
@RequestScoped
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public class CustomerResource {
|
||||
|
||||
private final CustomerService customerService;
|
||||
|
||||
protected CustomerResource() {
|
||||
this.customerService = null;
|
||||
}
|
||||
|
||||
@Inject
|
||||
public CustomerResource(CustomerService customerService) {
|
||||
this.customerService = customerService;
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/{id}")
|
||||
public Response findById(@PathParam("id") String id) {
|
||||
return customerService.findById(id)
|
||||
.map(customer -> Response.ok(customer).build())
|
||||
.orElseGet(() -> Response.status(Response.Status.NOT_FOUND).build());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Service Layer
|
||||
|
||||
- **Transactions:** Define transaction boundaries around complete business operations.
|
||||
- **Entity Mapping:** Map persistence entities to API models at the service boundary so that service method signatures expose only API models. A service returning `Optional<CustomerEntity>` where the caller expects `Optional<Customer>` is a common generated-code compile error.
|
||||
- **Concurrency:** Avoid mutable shared state in application-scoped components unless access is properly coordinated.
|
||||
|
||||
### Helidon SE Example
|
||||
|
||||
Helidon SE services are normally plain Java classes with explicitly supplied dependencies.
|
||||
|
||||
```java
|
||||
public final class CustomerService {
|
||||
|
||||
private final CustomerRepository customerRepository;
|
||||
|
||||
public CustomerService(CustomerRepository customerRepository) {
|
||||
this.customerRepository = customerRepository;
|
||||
}
|
||||
|
||||
public Optional<Customer> findById(String id) {
|
||||
if (id == null || id.isBlank()) {
|
||||
throw new IllegalArgumentException("Customer ID is required");
|
||||
}
|
||||
|
||||
return customerRepository.findById(id);
|
||||
}
|
||||
|
||||
public Customer create(CreateCustomerRequest request) {
|
||||
if (request.name() == null || request.name().isBlank()) {
|
||||
throw new IllegalArgumentException("Customer name is required");
|
||||
}
|
||||
|
||||
var customer = new Customer(request.id(), request.name().trim());
|
||||
|
||||
customerRepository.save(customer);
|
||||
return customer;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Construct the dependency graph explicitly:
|
||||
|
||||
```java
|
||||
var repository = new DbCustomerRepository(dbClient);
|
||||
var service = new CustomerService(repository);
|
||||
var httpService = new CustomerHttpService(service);
|
||||
```
|
||||
|
||||
### Helidon MP Example
|
||||
|
||||
Use CDI scopes and constructor injection. Apply transactions at the service layer when a business operation changes persistent state. Map the persistence entity to the API model here, so the service never leaks `CustomerEntity` to callers.
|
||||
|
||||
```java
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.transaction.Transactional;
|
||||
|
||||
@ApplicationScoped
|
||||
public class CustomerService {
|
||||
|
||||
private final JpaCustomerRepository customerRepository;
|
||||
|
||||
protected CustomerService() {
|
||||
this.customerRepository = null;
|
||||
}
|
||||
|
||||
@Inject
|
||||
public CustomerService(JpaCustomerRepository customerRepository) {
|
||||
this.customerRepository = customerRepository;
|
||||
}
|
||||
|
||||
public Optional<Customer> findById(String id) {
|
||||
if (id == null || id.isBlank()) {
|
||||
throw new IllegalArgumentException("Customer ID is required");
|
||||
}
|
||||
|
||||
return customerRepository.findById(id)
|
||||
.map(Customer::fromEntity);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Customer create(CreateCustomerRequest request) {
|
||||
if (request.name() == null || request.name().isBlank()) {
|
||||
throw new IllegalArgumentException("Customer name is required");
|
||||
}
|
||||
|
||||
var entity = new CustomerEntity(request.id(), request.name().trim());
|
||||
|
||||
customerRepository.save(entity);
|
||||
return Customer.fromEntity(entity);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Data Layer
|
||||
|
||||
- **Database Access:** Use Helidon DB Client, Jakarta Persistence, or another persistence mechanism already established by the project.
|
||||
- **Parameterized Queries:** Always use parameter binding or prepared statements. Never concatenate untrusted input into SQL.
|
||||
- **Column Accessors:** Read a typed column value with `column("name").getString()` (or `getInt()`, `getLong()`, and so on) or with `column("name").get(String.class)`. `DbColumn.as(String.class)` returns an `OptionalValue<String>` in Helidon 4, not a `String`.
|
||||
- **Nullable Columns:** Read nullable columns through `asOptional()` or another optional-aware accessor. Direct `getString()` and similar accessors throw when the column value is null.
|
||||
- **Row Mapping:** For whole-row mapping, `DbRow.as(Customer.class)` returns the mapped instance directly, but requires a `DbMapper` registered through a `DbMapperProvider` service-loader entry. Prefer explicit column reads for a small number of simple repositories, and introduce a `DbMapper` when the same row shape is mapped in several places.
|
||||
- **Migrations:** Use a database migration tool for schema changes rather than automatic destructive schema updates.
|
||||
- **Entity Separation:** Do not expose database entities directly as API contracts.
|
||||
|
||||
### Helidon SE Example
|
||||
|
||||
Use Helidon DB Client with parameterized statements. Map database rows into application models inside the repository.
|
||||
|
||||
```java
|
||||
import io.helidon.dbclient.DbClient;
|
||||
|
||||
public final class DbCustomerRepository implements CustomerRepository {
|
||||
|
||||
private static final String FIND_BY_ID =
|
||||
"SELECT id, name FROM customers WHERE id = :id";
|
||||
|
||||
private static final String INSERT =
|
||||
"INSERT INTO customers (id, name) VALUES (:id, :name)";
|
||||
|
||||
private final DbClient dbClient;
|
||||
|
||||
public DbCustomerRepository(DbClient dbClient) {
|
||||
this.dbClient = dbClient;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Customer> findById(String id) {
|
||||
return dbClient.execute()
|
||||
.createGet(FIND_BY_ID)
|
||||
.addParam("id", id)
|
||||
.execute()
|
||||
.map(row -> new Customer(
|
||||
row.column("id").getString(),
|
||||
row.column("name").getString()
|
||||
));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(Customer customer) {
|
||||
dbClient.execute()
|
||||
.createInsert(INSERT)
|
||||
.addParam("id", customer.id())
|
||||
.addParam("name", customer.name())
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Named statements can also be stored in configuration instead of embedding SQL in Java:
|
||||
|
||||
```yaml
|
||||
db:
|
||||
source: "jdbc"
|
||||
connection:
|
||||
url: "jdbc:postgresql://localhost:5432/customers"
|
||||
username: ${DB_USERNAME}
|
||||
password: ${DB_PASSWORD}
|
||||
statements:
|
||||
find-customer-by-id: >
|
||||
SELECT id, name
|
||||
FROM customers
|
||||
WHERE id = :id
|
||||
```
|
||||
|
||||
Reference the named statement by name instead of passing SQL text:
|
||||
|
||||
```java
|
||||
return dbClient.execute()
|
||||
.createNamedGet("find-customer-by-id")
|
||||
.addParam("id", id)
|
||||
.execute()
|
||||
.map(row -> new Customer(
|
||||
row.column("id").getString(),
|
||||
row.column("name").getString()
|
||||
));
|
||||
```
|
||||
|
||||
### Helidon MP Example
|
||||
|
||||
Use Jakarta Persistence in a CDI-managed repository. Keep transaction boundaries in the service layer. The repository works in entities; the service maps them to API models.
|
||||
|
||||
```java
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.PersistenceContext;
|
||||
|
||||
@ApplicationScoped
|
||||
public class JpaCustomerRepository {
|
||||
|
||||
@PersistenceContext
|
||||
private EntityManager entityManager;
|
||||
|
||||
public Optional<CustomerEntity> findById(String id) {
|
||||
return Optional.ofNullable(entityManager.find(CustomerEntity.class, id));
|
||||
}
|
||||
|
||||
public void save(CustomerEntity customer) {
|
||||
entityManager.persist(customer);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Define the persistence entity separately from the public API model:
|
||||
|
||||
```java
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
@Entity
|
||||
@Table(name = "customers")
|
||||
public class CustomerEntity {
|
||||
|
||||
@Id
|
||||
private String id;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String name;
|
||||
|
||||
protected CustomerEntity() {
|
||||
}
|
||||
|
||||
public CustomerEntity(String id, String name) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String id() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String name() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The API model stays free of persistence annotations and owns the mapping:
|
||||
|
||||
```java
|
||||
public record Customer(String id, String name) {
|
||||
|
||||
public static Customer fromEntity(CustomerEntity entity) {
|
||||
return new Customer(entity.id(), entity.name());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Observability
|
||||
|
||||
- **Health:** Use Helidon Health in SE or MicroProfile Health in MP for liveness and readiness checks.
|
||||
- **Metrics:** Use Helidon Metrics or MicroProfile Metrics for operational and business measurements.
|
||||
- **Tracing:** Propagate tracing context across inbound and outbound service calls.
|
||||
- **Cardinality:** Avoid user IDs, request IDs, email addresses, and raw URLs as metric tags.
|
||||
|
||||
## Logging
|
||||
|
||||
- **Logging API:** Use the logging API and implementation configured by the project.
|
||||
- **Sensitive Information:** Never log passwords, access tokens, authorization headers, cookies, or complete sensitive request bodies. Do not place secrets or personal information in metrics or trace attributes either.
|
||||
|
||||
## Testing
|
||||
|
||||
- **Unit Tests:** Write unit tests for business services using JUnit 5.
|
||||
- **Helidon SE Tests:** Use `helidon-webserver-testing-junit5` with `@ServerTest` for full server tests and `@RoutingTest` for routing-only tests. These start the server on a dynamically selected port and inject a `Http1Client` bound to it. Never hardcode a port.
|
||||
- **Helidon MP Tests:** Use `helidon-microprofile-testing-junit5` with `@HelidonTest`, which starts the CDI container and server for the test class. Confirm the artifact coordinates against the Helidon version in use, since this module was renamed across 4.x releases.
|
||||
- **Testcontainers:** Consider Testcontainers for integration tests using real databases, message brokers, or other infrastructure.
|
||||
- **Failure Paths:** Test validation failures, missing resources, external-service failures, and authorization failures.
|
||||
|
||||
## Security
|
||||
|
||||
- **Helidon Security:** Use Helidon Security or supported Jakarta and MicroProfile security APIs for authentication and authorization.
|
||||
- **Authorization:** Enforce permissions at a clear application boundary and deny protected operations by default.
|
||||
- **JWT and OIDC:** Validate token signatures, issuers, audiences, and expiration times.
|
||||
- **TLS:** Use TLS for production traffic and verify certificates for outbound connections.
|
||||
- **CORS:** Configure allowed origins explicitly. Do not combine wildcard origins with credentials.
|
||||
- **Secrets:** Store secrets in protected environment configuration or a dedicated secret-management system.
|
||||
- **Outbound Requests:** Validate outbound destinations to reduce server-side request forgery risks.
|
||||
Reference in New Issue
Block a user