Apply Helidon 4 SE and MP best practices for Java 21 applications, including routing, DB Client, Jakarta and MicroProfile APIs, configuration, security, observability, and tests. Use when working with Helidon SE, Helidon MP, HttpService, HttpRules, MicroProfile Config, Helidon DB Client, Helidon Security, or Helidon testing.
Guide Helidon 4 code generation and review so SE and MP applications compile, keep business logic out of transport layers, use Java 21 idioms, bind data safely, and avoid common Helidon 3 API mistakes.
When to invoke
"Write a Helidon 4 SE service."
"Review this Helidon MP resource."
"Fix Helidon DB Client code that does not compile."
"Add Helidon tests without hardcoding a port."
"Migrate Helidon 3 APIs to Helidon 4."
Helidon 3 to 4 API changes
Check generated code against this table before returning it. The left column commonly appears in older examples and does not compile or is wrong for Helidon 4.
dbClient.execute() returning Optional<DbRow> or Stream<DbRow>
javax.*
jakarta.*
helidon-microprofile-tests-junit5
helidon-microprofile-testing-junit5
Value.as(Class) returns OptionalValue<T>, not T; DbColumn.as(String.class) returns OptionalValue<String>, not String. This is the most common generated-code compile error in high-quality Helidon 4 work.
Migration tokens to verify explicitly: DbColumn.as(String.class), OptionalValue<String>, String.
Project setup and programming model
Concern
Rule
Programming model
Determine whether the repository uses Helidon SE or Helidon MP before generating code; do not mix them unless explicitly required.
Java version
Use Java 21 or later for Helidon 4.
Build
Use the existing pom.xml or build.gradle; align Helidon versions with the Helidon BOM or platform.
Packages
Organize by feature or domain, such as com.example.app.order and com.example.app.customer, not only technical layers.
Configuration files
Store non-secret configuration in application.yaml or application.properties; use environment-dependent and deployment-specific overrides for runtime values.
Secrets
Never hardcode credentials, API keys, tokens, private certificates, DB_USERNAME, or DB_PASSWORD.
Use the project's secret-management system for production credentials.
Helidon SE patterns
Layer
Rule
Bootstrap
Compose dependencies explicitly in the application startup layer.
Services
Use constructor injection with private final fields.
Routing
Group related routes in focused HttpService classes and register them with routing.register(...).
Request handling
Keep handlers small; validate parameters and delegate business logic.
Concurrency
Prefer straightforward blocking code on Helidon 4 virtual-thread-based request handling. Do not generate Single, Multi, or CompletionStage chains without a project-specific reason.
SE route shape:
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 with WebServer.builder().routing(routing -> routing.register("/customers", customerHttpService)).build().start();.
Helidon MP patterns
Layer
Rule
Standards
Prefer Jakarta EE and Eclipse MicroProfile APIs when available.
Injection
Use CDI constructor injection for required dependencies.
Scopes
Choose @ApplicationScoped and @RequestScoped intentionally.
Normal-scoped beans
Add a non-privateno-argument constructor to normal-scoped beans that also use constructor injection so CDI proxies can be created.
REST resources
Keep REST resources thin and delegate business operations to service classes.
Portability
Use portable Jakarta and MicroProfile APIs when portability matters.
Use request and response models; never expose persistence entities directly through APIs.
Validation
Validate path parameters, query parameters, headers, and bodies before business logic.
Status codes
Return appropriate HTTP status codes; on PUT and DELETE, return 404 when the target does not exist.
Errors
Use centralized error handling in SE and Jakarta REST ExceptionMapper in MP. Do not expose stack traces, database details, filesystem paths, or internal exception messages.
Transactions
Put transaction boundaries around complete business operations.
Mapping
Map persistence entities to API models at the service boundary; avoid Optional<CustomerEntity> leaking where callers expect Optional<Customer>.
State
Avoid mutable shared state in application-scoped components unless access is coordinated.
A service can throw IllegalArgumentException for blank IDs or invalid CreateCustomerRequest data, map CustomerEntity to Customer.fromEntity, and use @Transactional on methods that change persistent state.
Data layer rules
Concern
Rule
Access technology
Use Helidon DB Client, Jakarta Persistence, or the persistence mechanism already established by the project.
SQL safety
Always bind parameters or use prepared statements; never concatenate untrusted input into SQL.
Column reads
Use column("name").getString(), column("name").get(String.class), getInt(), getLong(), or other typed accessors.
Nullability
Use asOptional() or optional-aware accessors for nullable columns; direct getString() throws for null.
Whole-row mapping
DbRow.as(Customer.class) returns the mapped instance directly but requires a DbMapper registered through a DbMapperProvider service-loader entry; use this whole-row path only when repeated row shapes justify it.
Migrations
Use a migration tool for schema changes; do not rely on destructive automatic schema updates.
Entity separation
Keep CustomerEntity and public API records separate.
Helidon DB Client repository shape:
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 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()));
}
}
For Helidon MP persistence, keep JpaCustomerRepository CDI-managed, inject EntityManager with @PersistenceContext, and return entities only inside the data layer.
Named statements may live in config:
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 named SQL with createNamedGet("find-customer-by-id") and bound parameters.
Observability, logging, testing, and security
Area
Rule
Health
Use Helidon Health in SE or MicroProfile Health in MP for liveness and readiness.
Metrics
Use Helidon Metrics or MicroProfile Metrics; avoid user IDs, request IDs, email addresses, and raw URLs as metric tags.
Tracing
Propagate tracing context across inbound and outbound service calls.
Logging
Use the project's logging API; never log passwords, access tokens, authorization headers, cookies, sensitive bodies, secrets, or personal data.
Unit tests
Use JUnit 5 for services.
SE tests
Use helidon-webserver-testing-junit5 with @ServerTest for full server tests or @RoutingTest for routing-only tests; inject Http1Client and never hardcode a port.
MP tests
Use helidon-microprofile-testing-junit5 with @HelidonTest; confirm coordinates because the artifact was renamed across 4.x releases.
Integration tests
Consider Testcontainers for real databases, brokers, or infrastructure.
Failure paths
Test validation failures, missing resources, external-service failures, and authorization failures.
Authentication
Use Helidon Security or supported Jakarta and MicroProfile security APIs.
Authorization
Deny protected operations by default and enforce permissions at a clear boundary.
JWT/OIDC
Validate token signatures, issuers, audiences, and expirations.
TLS
Use TLS in production and verify outbound certificates.
CORS
Configure allowed origins explicitly; do not combine wildcard origins with credentials.
Outbound requests
Validate destinations to reduce server-side request forgery risk.
Gotchas
Value.as(Class) is not a direct value: unwrap OptionalValue<T> or use typed accessors.
Helidon 4 is not the old reactive API: avoid Single, Multi, and CompletionStage patterns copied from Helidon 3.
CDI proxy construction matters: normal-scoped MP beans with constructor injection need a non-private no-argument constructor.
Testing ports must be dynamic: @ServerTest, @RoutingTest, and @HelidonTest manage server lifecycle; do not hardcode ports.
Output template
## Helidon result
**Status:** complete | needs changes | blocked
**Programming model:** SE | MP
**Files reviewed or generated:** <paths>
### Findings or changes
| Area | Evidence | Action |
| --- | --- | --- |
| API migration | `<old API>` | `<Helidon 4 replacement>` |
| Web layer | `<route/resource evidence>` | `<fix>` |
| Data layer | `<query or mapper evidence>` | `<fix>` |
| Tests | `<test evidence>` | `<fix>` |
### Validation
- Compile check: pass | fail | not run
- Tests: pass | fail | not run
Quality gate
The output identifies Helidon SE or Helidon MP and does not mix models accidentally.
No Helidon 3 APIs from the migration table remain in generated code.
Java 21, aligned Helidon dependencies, and existing pom.xml or build.gradle conventions are respected.
Route/resource classes validate inputs, return correct HTTP statuses, and delegate business logic.
SQL uses bound parameters and safe typed accessors; nullable columns use optional-aware reads.
Entities are separated from API DTOs or records.
Configuration and secrets are externalized through application.yaml, application.properties, environment, or secret management.
Tests use the correct Helidon testing artifact and dynamic server ports.
Security, logging, metrics, CORS, OIDC, and TLS guidance is applied where relevant.
1---2name: java-helidon3description: Apply Helidon 4 SE and MP best practices for Java 21 applications, including routing, DB Client, Jakarta and MicroProfile APIs, configuration, security, observability, and tests. Use when working with Helidon SE, Helidon MP, HttpService, HttpRules, MicroProfile Config, Helidon DB Client, Helidon Security, or Helidon testing.4---56<!-- Generated from harness/github-copilot/skills/java-helidon/SKILL.md by harness/claude-code/scripts/convert_from_copilot.py. Edit the source, not this file. -->78# Java Helidon910Guide Helidon 4 code generation and review so SE and MP applications compile, keep business logic out of transport layers, use Java 21 idioms, bind data safely, and avoid common Helidon 3 API mistakes.1112## When to invoke1314- "Write a Helidon 4 SE service."15- "Review this Helidon MP resource."16- "Fix Helidon DB Client code that does not compile."17- "Add Helidon tests without hardcoding a port."18- "Migrate Helidon 3 APIs to Helidon 4."1920## Helidon 3 to 4 API changes2122Check generated code against this table before returning it. The left column commonly appears in older examples and does not compile or is wrong for Helidon 4.2324| Do not use | Use in Helidon 4 |25| --- | --- |26| `io.helidon.common.http.Http.Status` | `io.helidon.http.Status` |27| `io.helidon.webserver.Service` | `io.helidon.webserver.http.HttpService` |28| `Routing.Rules`, `update(Routing.Rules)` | `HttpRules`, `routing(HttpRules)` |29| `request.path().param("id")` | `request.path().pathParameters().get("id")` |30| `String s = column.as(String.class)` | `column.getString()` or `column.get(String.class)` |31| `dbClient.execute(exec -> ...)` returning `Single`/`Multi` | `dbClient.execute()` returning `Optional<DbRow>` or `Stream<DbRow>` |32| `javax.*` | `jakarta.*` |33| `helidon-microprofile-tests-junit5` | `helidon-microprofile-testing-junit5` |3435`Value.as(Class)` returns `OptionalValue<T>`, not `T`; `DbColumn.as(String.class)` returns `OptionalValue<String>`, not `String`. This is the most common generated-code compile error in high-quality Helidon 4 work.3637Migration tokens to verify explicitly: `DbColumn.as(String.class)`, `OptionalValue<String>`, `String`.3839## Project setup and programming model4041| Concern | Rule |42| --- | --- |43| Programming model | Determine whether the repository uses Helidon SE or Helidon MP before generating code; do not mix them unless explicitly required. |44| Java version | Use Java 21 or later for Helidon 4. |45| Build | Use the existing `pom.xml` or `build.gradle`; align Helidon versions with the Helidon BOM or platform. |46| Packages | Organize by feature or domain, such as `com.example.app.order` and `com.example.app.customer`, not only technical layers. |47| Configuration files | Store non-secret configuration in `application.yaml` or `application.properties`; use environment-dependent and deployment-specific overrides for runtime values. |48| Secrets | Never hardcode credentials, API keys, tokens, private certificates, `DB_USERNAME`, or `DB_PASSWORD`. |4950Use the project's secret-management system for production credentials.5152## Helidon SE patterns5354| Layer | Rule |55| --- | --- |56| Bootstrap | Compose dependencies explicitly in the application startup layer. |57| Services | Use constructor injection with `private final` fields. |58| Routing | Group related routes in focused `HttpService` classes and register them with `routing.register(...)`. |59| Request handling | Keep handlers small; validate parameters and delegate business logic. |60| Concurrency | Prefer straightforward blocking code on Helidon 4 virtual-thread-based request handling. Do not generate `Single`, `Multi`, or `CompletionStage` chains without a project-specific reason. |6162SE route shape:6364```java65import io.helidon.http.Status;66import io.helidon.webserver.http.HttpRules;67import io.helidon.webserver.http.HttpService;68import io.helidon.webserver.http.ServerRequest;69import io.helidon.webserver.http.ServerResponse;7071public final class CustomerHttpService implements HttpService {72 private final CustomerService customerService;7374 public CustomerHttpService(CustomerService customerService) {75 this.customerService = customerService;76 }7778 @Override79 public void routing(HttpRules rules) {80 rules.get("/{id}", this::findById);81 }8283 private void findById(ServerRequest request, ServerResponse response) {84 var id = request.path().pathParameters().get("id");85 customerService.findById(id)86 .ifPresentOrElse(response::send, () -> response.status(Status.NOT_FOUND_404).send());87 }88}89```9091Register with `WebServer.builder().routing(routing -> routing.register("/customers", customerHttpService)).build().start();`.9293## Helidon MP patterns9495| Layer | Rule |96| --- | --- |97| Standards | Prefer Jakarta EE and Eclipse MicroProfile APIs when available. |98| Injection | Use CDI constructor injection for required dependencies. |99| Scopes | Choose `@ApplicationScoped` and `@RequestScoped` intentionally. |100| Normal-scoped beans | Add a `non-private` `no-argument` constructor to normal-scoped beans that also use constructor injection so CDI proxies can be created. |101| REST resources | Keep REST resources thin and delegate business operations to service classes. |102| Portability | Use portable Jakarta and MicroProfile APIs when portability matters. |103104MP resource shape:105106```java107import jakarta.enterprise.context.RequestScoped;108import jakarta.inject.Inject;109import jakarta.ws.rs.GET;110import jakarta.ws.rs.Path;111import jakarta.ws.rs.PathParam;112import jakarta.ws.rs.Produces;113import jakarta.ws.rs.core.MediaType;114import jakarta.ws.rs.core.Response;115116@Path("/customers")117@RequestScoped118@Produces(MediaType.APPLICATION_JSON)119public class CustomerResource {120 private final CustomerService customerService;121122 protected CustomerResource() { this.customerService = null; }123124 @Inject125 public CustomerResource(CustomerService customerService) { this.customerService = customerService; }126127 @GET128 @Path("/{id}")129 public Response findById(@PathParam("id") String id) {130 return customerService.findById(id)131 .map(customer -> Response.ok(customer).build())132 .orElseGet(() -> Response.status(Response.Status.NOT_FOUND).build());133 }134}135```136137## Web and service layer rules138139| Area | Rule |140| --- | --- |141| DTOs | Use request and response models; never expose persistence entities directly through APIs. |142| Validation | Validate path parameters, query parameters, headers, and bodies before business logic. |143| Status codes | Return appropriate HTTP status codes; on `PUT` and `DELETE`, return 404 when the target does not exist. |144| Errors | Use centralized error handling in SE and Jakarta REST `ExceptionMapper` in MP. Do not expose stack traces, database details, filesystem paths, or internal exception messages. |145| Transactions | Put transaction boundaries around complete business operations. |146| Mapping | Map persistence entities to API models at the service boundary; avoid `Optional<CustomerEntity>` leaking where callers expect `Optional<Customer>`. |147| State | Avoid mutable shared state in application-scoped components unless access is coordinated. |148149A service can throw `IllegalArgumentException` for blank IDs or invalid `CreateCustomerRequest` data, map `CustomerEntity` to `Customer.fromEntity`, and use `@Transactional` on methods that change persistent state.150151## Data layer rules152153| Concern | Rule |154| --- | --- |155| Access technology | Use Helidon DB Client, Jakarta Persistence, or the persistence mechanism already established by the project. |156| SQL safety | Always bind parameters or use prepared statements; never concatenate untrusted input into SQL. |157| Column reads | Use `column("name").getString()`, `column("name").get(String.class)`, `getInt()`, `getLong()`, or other typed accessors. |158| Nullability | Use `asOptional()` or optional-aware accessors for nullable columns; direct `getString()` throws for null. |159| Whole-row mapping | `DbRow.as(Customer.class)` returns the mapped instance directly but requires a `DbMapper` registered through a `DbMapperProvider` service-loader entry; use this whole-row path only when repeated row shapes justify it. |160| Migrations | Use a migration tool for schema changes; do not rely on destructive automatic schema updates. |161| Entity separation | Keep `CustomerEntity` and public API records separate. |162163Helidon DB Client repository shape:164165```java166import io.helidon.dbclient.DbClient;167168public final class DbCustomerRepository implements CustomerRepository {169 private static final String FIND_BY_ID = "SELECT id, name FROM customers WHERE id = :id";170 private static final String INSERT = "INSERT INTO customers (id, name) VALUES (:id, :name)";171 private final DbClient dbClient;172173 public Optional<Customer> findById(String id) {174 return dbClient.execute()175 .createGet(FIND_BY_ID)176 .addParam("id", id)177 .execute()178 .map(row -> new Customer(row.column("id").getString(), row.column("name").getString()));179 }180}181```182183For Helidon MP persistence, keep `JpaCustomerRepository` CDI-managed, inject `EntityManager` with `@PersistenceContext`, and return entities only inside the data layer.184185Named statements may live in config:186187```yaml188db:189 source: "jdbc"190 connection:191 url: "jdbc:postgresql://localhost:5432/customers"192 username: ${DB_USERNAME}193 password: ${DB_PASSWORD}194 statements:195 find-customer-by-id: >196 SELECT id, name197 FROM customers198 WHERE id = :id199```200201Reference named SQL with `createNamedGet("find-customer-by-id")` and bound parameters.202203## Observability, logging, testing, and security204205| Area | Rule |206| --- | --- |207| Health | Use Helidon Health in SE or MicroProfile Health in MP for liveness and readiness. |208| Metrics | Use Helidon Metrics or MicroProfile Metrics; avoid user IDs, request IDs, email addresses, and raw URLs as metric tags. |209| Tracing | Propagate tracing context across inbound and outbound service calls. |210| Logging | Use the project's logging API; never log passwords, access tokens, authorization headers, cookies, sensitive bodies, secrets, or personal data. |211| Unit tests | Use JUnit 5 for services. |212| SE tests | Use `helidon-webserver-testing-junit5` with `@ServerTest` for full server tests or `@RoutingTest` for routing-only tests; inject `Http1Client` and never hardcode a port. |213| MP tests | Use `helidon-microprofile-testing-junit5` with `@HelidonTest`; confirm coordinates because the artifact was renamed across 4.x releases. |214| Integration tests | Consider Testcontainers for real databases, brokers, or infrastructure. |215| Failure paths | Test validation failures, missing resources, external-service failures, and authorization failures. |216| Authentication | Use Helidon Security or supported Jakarta and MicroProfile security APIs. |217| Authorization | Deny protected operations by default and enforce permissions at a clear boundary. |218| JWT/OIDC | Validate token signatures, issuers, audiences, and expirations. |219| TLS | Use TLS in production and verify outbound certificates. |220| CORS | Configure allowed origins explicitly; do not combine wildcard origins with credentials. |221| Outbound requests | Validate destinations to reduce server-side request forgery risk. |222223## Gotchas224225- **`Value.as(Class)` is not a direct value**: unwrap `OptionalValue<T>` or use typed accessors.226- **Helidon 4 is not the old reactive API**: avoid `Single`, `Multi`, and `CompletionStage` patterns copied from Helidon 3.227- **CDI proxy construction matters**: normal-scoped MP beans with constructor injection need a non-private no-argument constructor.228- **Testing ports must be dynamic**: `@ServerTest`, `@RoutingTest`, and `@HelidonTest` manage server lifecycle; do not hardcode ports.229230## Output template231232```markdown233## Helidon result234235**Status:** complete | needs changes | blocked236**Programming model:** SE | MP237**Files reviewed or generated:** <paths>238239### Findings or changes240| Area | Evidence | Action |241| --- | --- | --- |242| API migration | `<old API>` | `<Helidon 4 replacement>` |243| Web layer | `<route/resource evidence>` | `<fix>` |244| Data layer | `<query or mapper evidence>` | `<fix>` |245| Tests | `<test evidence>` | `<fix>` |246247### Validation248- Compile check: pass | fail | not run249- Tests: pass | fail | not run250```251252## Quality gate253254- [ ] The output identifies Helidon SE or Helidon MP and does not mix models accidentally.255- [ ] No Helidon 3 APIs from the migration table remain in generated code.256- [ ] Java 21, aligned Helidon dependencies, and existing `pom.xml` or `build.gradle` conventions are respected.257- [ ] Route/resource classes validate inputs, return correct HTTP statuses, and delegate business logic.258- [ ] SQL uses bound parameters and safe typed accessors; nullable columns use optional-aware reads.259- [ ] Entities are separated from API DTOs or records.260- [ ] Configuration and secrets are externalized through `application.yaml`, `application.properties`, environment, or secret management.261- [ ] Tests use the correct Helidon testing artifact and dynamic server ports.262- [ ] Security, logging, metrics, CORS, OIDC, and TLS guidance is applied where relevant.
Run npx skillmds@latest add paulasilvatech/java-helidon in your terminal (requires Node.js), paste this page's agent-chat prompt into Claude, Cursor, or any MCP-connected agent, or download the SKILL.md file and copy it into your agent's skills directory.
Apply Helidon 4 SE and MP best practices for Java 21 applications, including routing, DB Client, Jakarta and MicroProfile APIs, configuration, security, observability, and tests. Use when working with Helidon SE, Helidon MP, HttpService, HttpRules, MicroProfile Config, Helidon DB Client, Helidon Security, or Helidon testing. It is listed under Security on SkillMD.
This skill has not completed SkillMD's automated safety review yet. SkillMD never runs a skill's scripts for you; review the SKILL.md before installing.
This skill is tagged as working with Claude Code, Claude.ai, OpenAI Codex. SKILL.md is an open format, so most agents that read a skills directory can load it too.
Yes. Installing skills from SkillMD is free, and the skill stays under its author's original license.
paulasilvatech (@paulasilvatech) published this skill. Their other Agent Skills are listed on their SkillMD profile.