Resource Methods and Annotation Surface
Resource Methods and JAX-RS Annotation Surface
Annotation inti untuk membuat endpoint JAX-RS yang eksplisit, aman, dan mudah direview
Part 009 — Resource Methods and JAX-RS Annotation Surface
Fokus part ini: memahami annotation surface JAX-RS yang mengubah class Java biasa menjadi HTTP resource yang bisa dipanggil oleh client.
Sampai part sebelumnya, kita sudah membangun mental model bahwa JAX-RS runtime melakukan matching request ke resource method. Sekarang kita masuk ke permukaan API yang paling sering dilihat di codebase:
@Path("/quotes")
public class QuoteResource {
@GET
@Path("/{quoteId}")
@Produces(MediaType.APPLICATION_JSON)
public QuoteDto getQuote(@PathParam("quoteId") String quoteId) {
...
}
}
Pada level syntax, ini terlihat sederhana. Pada level production, setiap annotation adalah bagian dari kontrak:
- path adalah kontrak routing,
- HTTP method adalah kontrak semantics,
- media type adalah kontrak representasi,
- parameter annotation adalah kontrak input,
Responseatau return type adalah kontrak output,- context injection adalah kontrak terhadap runtime,
- exception behavior adalah kontrak failure.
Tujuan part ini bukan menghafal semua annotation, tetapi membangun cara membaca resource method seperti senior engineer:
What HTTP contract does this Java method expose?
What assumptions does it make?
What can break when the contract evolves?
1. Resource Method Is Not Just a Java Method
Resource method adalah method Java yang dipanggil oleh runtime setelah request melewati pipeline:
HTTP request
-> path/method/media matching
-> filter/interceptor
-> parameter binding
-> body deserialization
-> validation
-> resource method invocation
-> response serialization
Artinya, resource method berada di boundary antara:
Network contract <-> Java object model <-> Application logic
Resource method yang baik tidak boleh menjadi tempat semua logic. Ia sebaiknya menjadi thin boundary yang melakukan:
- menerima input transport,
- memanggil application service/use case,
- memetakan hasil ke response HTTP,
- membiarkan cross-cutting concern ditangani filter/provider/mapper yang tepat.
Contoh yang lebih sehat:
@Path("/quotes")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class QuoteResource {
private final QuoteApplicationService quoteService;
public QuoteResource(QuoteApplicationService quoteService) {
this.quoteService = quoteService;
}
@POST
public Response createQuote(CreateQuoteRequest request,
@Context UriInfo uriInfo) {
QuoteId quoteId = quoteService.createQuote(request.toCommand());
URI location = uriInfo.getAbsolutePathBuilder()
.path(quoteId.value())
.build();
return Response.created(location)
.entity(new CreateQuoteResponse(quoteId.value()))
.build();
}
}
Perhatikan boundary-nya:
- resource menerima DTO request,
- resource tidak langsung menjalankan SQL,
- resource tidak mengerti detail Kafka,
- resource tidak menulis audit log manual kecuali memang boundary-nya di sana,
- resource mengembalikan status HTTP yang sesuai.
2. Class-Level @Path
@Path pada class mendefinisikan root resource path.
@Path("/quotes")
public class QuoteResource {
}
Jika application path adalah /api, maka path efektif biasanya:
/api/quotes
Namun public URL bisa berbeda karena:
- servlet context path,
- ingress prefix,
- API gateway route,
- reverse proxy rewrite,
- environment-specific base URL,
- versioning prefix.
Jangan membaca @Path sebagai public URL final. Baca sebagai internal JAX-RS resource path.
Good Practice
Gunakan path yang merepresentasikan resource, bukan action internal.
Lebih baik:
@Path("/quotes")
Daripada:
@Path("/quoteOperations")
@Path("/quoteService")
@Path("/doQuoteAction")
Untuk command yang memang aksi domain, tetap hati-hati:
@POST
@Path("/{quoteId}/submit")
Ini bisa valid jika submit adalah transition domain yang jelas. Tapi jangan menjadikan semua endpoint sebagai RPC tersembunyi:
POST /quotes/createQuote
POST /quotes/updateQuote
POST /quotes/deleteQuote
POST /quotes/searchQuote
Kalau semua endpoint berbentuk verb, biasanya API kehilangan semantic HTTP dan menjadi RPC-over-HTTP.
3. Method-Level @Path
@Path pada method menambahkan segment di bawah class-level path.
@Path("/quotes")
public class QuoteResource {
@GET
@Path("/{quoteId}")
public QuoteDto getQuote(@PathParam("quoteId") String quoteId) {
...
}
}
Path efektif:
/quotes/{quoteId}
Method-level @Path bisa kosong atau tidak ada.
@GET
public List<QuoteSummaryDto> listQuotes() {
...
}
Path efektif:
GET /quotes
Avoid Ambiguous Path Design
Hati-hati dengan kombinasi seperti:
@GET
@Path("/{id}")
public QuoteDto getById(@PathParam("id") String id) { ... }
@GET
@Path("/search")
public SearchResult search() { ... }
Tergantung matching rules dan specificity, /search bisa aman, tetapi desain seperti ini perlu direview karena search juga bisa terlihat seperti {id} jika path lebih generik.
Lebih eksplisit:
@GET
@Path("/{quoteId}")
public QuoteDto getById(...) { ... }
@GET
@Path("/_search")
public SearchResult search(...) { ... }
Atau gunakan query pattern:
GET /quotes?customerId=...&status=...
Untuk enterprise API, preferensi harus mengikuti internal API governance.
4. HTTP Method Annotations
JAX-RS menyediakan annotation untuk method HTTP umum:
@GET
@POST
@PUT
@PATCH
@DELETE
@HEAD
@OPTIONS
Tidak semua codebase memakai semua method. Beberapa organisasi membatasi penggunaan PATCH, DELETE, atau custom method karena constraint gateway, audit, security, atau consumer compatibility.
Semantics Ringkas
| Annotation | HTTP Semantics | Umum Digunakan Untuk | Catatan Review |
|---|---|---|---|
@GET | safe, read-only | read/list/search | tidak boleh mutate state |
@POST | non-idempotent by default | create, command, submit | perlu idempotency jika retryable |
@PUT | idempotent replace/upsert | replace resource | body harus cukup lengkap |
@PATCH | partial update | patch fields | butuh patch semantics jelas |
@DELETE | idempotent delete intent | delete/cancel | physical vs logical delete harus jelas |
@HEAD | metadata only | check existence/cache | response body tidak dikirim |
@OPTIONS | capability/preflight | CORS/preflight | sering ditangani platform |
Senior Review Question
Untuk setiap method annotation, tanyakan:
Does this Java method use HTTP semantics correctly?
Contoh buruk:
@GET
@Path("/{quoteId}/submit")
public QuoteDto submitQuote(@PathParam("quoteId") String quoteId) {
return quoteService.submit(quoteId);
}
Masalah:
GETseharusnya safe,- proxy/browser/client bisa retry atau prefetch,
- audit dan observability menjadi misleading,
- operation mutation tersembunyi di read method.
Lebih baik:
@POST
@Path("/{quoteId}/submit")
public Response submitQuote(@PathParam("quoteId") String quoteId,
SubmitQuoteRequest request) {
QuoteDto quote = quoteService.submit(quoteId, request.toCommand());
return Response.ok(quote).build();
}
5. @Consumes and Content-Type
@Consumes menyatakan media type request body yang diterima.
@POST
@Consumes(MediaType.APPLICATION_JSON)
public Response createQuote(CreateQuoteRequest request) {
...
}
Client harus mengirim:
Content-Type: application/json
Jika Content-Type tidak cocok, runtime bisa mengembalikan 415 Unsupported Media Type.
Class-Level vs Method-Level
Class-level:
@Path("/quotes")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class QuoteResource {
...
}
Method-level override:
@POST
@Path("/import")
@Consumes("text/csv")
public Response importQuotes(InputStream csvStream) {
...
}
Gunakan class-level default untuk consistency, method-level override untuk endpoint khusus.
Anti-Pattern: Missing @Consumes
Jika tidak eksplisit, runtime/provider bisa memilih behavior default yang tidak terlihat jelas.
@POST
public Response create(CreateQuoteRequest request) {
...
}
Ini mungkin tetap bekerja, tetapi untuk API production lebih baik eksplisit.
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response create(CreateQuoteRequest request) {
...
}
6. @Produces and Accept
@Produces menyatakan media type response yang bisa dihasilkan.
@GET
@Path("/{quoteId}")
@Produces(MediaType.APPLICATION_JSON)
public QuoteDto getQuote(...) {
...
}
Client bisa mengirim:
Accept: application/json
Jika tidak ada response representation yang cocok dengan Accept, runtime bisa mengembalikan 406 Not Acceptable.
Multiple Representations
@GET
@Path("/{quoteId}")
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public QuoteDto getQuote(...) {
...
}
Ini berarti resource method bisa menghasilkan JSON atau XML tergantung negotiation.
Production concern:
- Apakah XML benar-benar didukung?
- Apakah DTO aman untuk JSON dan XML sekaligus?
- Apakah field naming sama?
- Apakah backward compatibility dijaga untuk semua representation?
- Apakah test mencakup semua media type?
Jangan menambahkan media type hanya karena library bisa. Setiap representation adalah contract.
7. Path Parameters with @PathParam
@GET
@Path("/{quoteId}")
public QuoteDto getQuote(@PathParam("quoteId") String quoteId) {
...
}
@PathParam mengambil value dari path template.
Type Conversion
@GET
@Path("/{version}")
public CatalogDto getCatalog(@PathParam("version") int version) {
...
}
Jika path value tidak bisa dikonversi ke int, request gagal sebelum method logic berjalan.
Untuk ID enterprise, sering lebih aman mulai dari String, lalu validasi eksplisit:
public QuoteDto getQuote(@PathParam("quoteId") String quoteId) {
QuoteId id = QuoteId.parse(quoteId);
return quoteService.get(id);
}
Keuntungan:
- error code bisa lebih domain-aware,
- format ID bisa divalidasi jelas,
- logging bisa lebih baik,
- tidak bergantung penuh pada conversion error generic.
Path Param Review Checklist
- Nama param sama dengan template?
- Format ID divalidasi?
- Case sensitivity jelas?
- Apakah ID tenant-scoped?
- Apakah path param bisa mengandung slash?
- Apakah URL encoding ditangani benar?
- Apakah error untuk invalid ID stabil?
8. Query Parameters with @QueryParam
@GET
public List<QuoteSummaryDto> listQuotes(
@QueryParam("status") String status,
@QueryParam("customerId") String customerId,
@QueryParam("limit") @DefaultValue("50") int limit) {
...
}
Query parameter cocok untuk:
- filtering,
- sorting,
- pagination,
- projection,
- include/exclude flags,
- search criteria sederhana.
Beware Primitive Defaults
@QueryParam("includeLines") boolean includeLines
Jika param hilang, value menjadi false. Itu bisa benar, tetapi kadang ambiguity:
false because client explicitly sent false?
false because client omitted parameter?
Jika distinction penting, gunakan wrapper:
@QueryParam("includeLines") Boolean includeLines
Atau jadikan request query object dengan defaulting eksplisit:
QuoteQuery query = QuoteQuery.builder()
.includeLines(Boolean.TRUE.equals(includeLines))
.build();
Pagination Safety
Jangan menerima limit bebas.
@QueryParam("limit") int limit
Lebih baik:
@QueryParam("limit") @DefaultValue("50") int limit
Lalu enforce max:
int safeLimit = Math.min(Math.max(limit, 1), 200);
Tetapi lebih baik lagi, aturan limit dimodelkan di query object agar konsisten lintas endpoint.
9. Header Parameters with @HeaderParam
@GET
@Path("/{quoteId}")
public QuoteDto getQuote(@PathParam("quoteId") String quoteId,
@HeaderParam("X-Correlation-Id") String correlationId,
@HeaderParam("X-Tenant-Id") String tenantId) {
...
}
Header umum di enterprise service:
- correlation ID,
- request ID,
- tenant ID,
- idempotency key,
- authorization token,
- locale,
- client application ID,
- API version,
- feature flag context.
Namun tidak semua header sebaiknya dibaca manual di setiap resource method.
Untuk cross-cutting header seperti correlation ID, tenant, auth, lebih baik ditangani di filter/context layer.
Inbound filter
-> validate/extract headers
-> build request context
-> put into SecurityContext / TenantContext / MDC
-> resource method reads typed context or service gets context explicitly
Jika setiap resource membaca header sendiri, hasilnya:
- duplicate logic,
- inconsistent validation,
- missing audit,
- tenant isolation risk,
- test sulit.
10. Cookie, Matrix, and Form Parameters
@CookieParam
@CookieParam("session") String sessionCookie
Biasanya lebih relevan untuk browser/session-oriented app. Untuk service-to-service API enterprise, auth lebih sering melalui Authorization header atau mTLS/service identity.
@MatrixParam
@Path("/catalogs/{catalogId}")
public CatalogDto get(@PathParam("catalogId") String id,
@MatrixParam("version") String version) {
...
}
Matrix param jarang dipakai di banyak enterprise API. Jika gateway/proxy tidak preserve semicolon path parameter, behavior bisa rusak.
@FormParam
@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response submit(@FormParam("username") String username,
@FormParam("password") String password) {
...
}
Form param cocok untuk form-encoded request, bukan JSON API biasa. Jangan mencampur form body dan JSON body tanpa alasan kuat.
11. @BeanParam for Grouped Parameters
Jika endpoint punya banyak query/header/path params, method signature bisa membengkak.
public List<QuoteDto> searchQuotes(
@QueryParam("customerId") String customerId,
@QueryParam("status") String status,
@QueryParam("from") String from,
@QueryParam("to") String to,
@QueryParam("limit") int limit,
@QueryParam("offset") int offset) {
...
}
@BeanParam bisa mengelompokkan parameter:
public class QuoteSearchParams {
@QueryParam("customerId")
public String customerId;
@QueryParam("status")
public String status;
@QueryParam("from")
public String from;
@QueryParam("to")
public String to;
@QueryParam("limit")
@DefaultValue("50")
public int limit;
@QueryParam("offset")
@DefaultValue("0")
public int offset;
}
Resource:
@GET
public QuoteSearchResponse search(@BeanParam QuoteSearchParams params) {
QuoteSearchQuery query = params.toQuery();
return quoteService.search(query);
}
Caution
@BeanParam can make the method cleaner, but can also hide the API surface. Use it when:
- params are cohesive,
- DTO has clear validation/defaulting,
- generated docs still show parameters,
- tests cover binding behavior.
12. @DefaultValue
@DefaultValue sets parameter value when missing.
@QueryParam("limit")
@DefaultValue("50")
int limit
Good for simple defaults. But for complex defaults, prefer explicit query object:
QuoteSearchQuery query = QuoteSearchQuery.from(params, defaultPolicy);
Why?
Because real enterprise defaults can depend on:
- tenant,
- product catalog,
- environment,
- feature flag,
- client type,
- API version,
- backward compatibility.
Do not hide complex business defaults inside annotation values.
13. Request Body Parameter
A resource method can have an entity parameter representing request body.
@POST
@Consumes(MediaType.APPLICATION_JSON)
public Response createQuote(CreateQuoteRequest request) {
...
}
The runtime uses a MessageBodyReader to deserialize request body into CreateQuoteRequest.
Important consequences:
- body can usually be consumed once,
- malformed JSON fails before service logic,
- unknown fields behavior depends on JSON provider config,
- date/time parsing depends on mapper config,
- validation may happen after deserialization,
- large body can pressure memory if buffered.
One Clear Body Model
Avoid method signatures where the source of truth is unclear:
@POST
@Path("/{quoteId}")
public Response updateQuote(@PathParam("quoteId") String quoteId,
UpdateQuoteRequest request) {
...
}
This is normal. But define rule:
Path param identifies target resource.
Body contains requested changes.
If both contain same field, path wins or request is rejected.
Bad pattern:
public Response updateQuote(@PathParam("quoteId") String quoteId,
UpdateQuoteRequest request) {
// request.quoteId may differ from path quoteId
}
Senior review should ask:
What happens if path quoteId != body quoteId?
Preferred options:
- body does not contain ID,
- body ID must match path ID,
- body ID is ignored and not exposed in DTO.
14. @Context and Runtime Objects
@Context injects runtime-provided context objects.
Common examples:
@Context UriInfo uriInfo
@Context HttpHeaders headers
@Context Request request
@Context SecurityContext securityContext
@Context ServletContext servletContext
@Context HttpServletRequest servletRequest
Example:
@POST
public Response createQuote(CreateQuoteRequest request,
@Context UriInfo uriInfo,
@Context SecurityContext securityContext) {
String actor = securityContext.getUserPrincipal().getName();
QuoteId id = quoteService.create(request.toCommand(actor));
URI location = uriInfo.getAbsolutePathBuilder()
.path(id.value())
.build();
return Response.created(location).build();
}
Use @Context Carefully
@Context is powerful but can couple resource logic to runtime details.
Healthy uses:
- build
LocationURI, - access authenticated principal,
- access request headers in one boundary,
- inspect request metadata,
- bridge to servlet only when needed.
Risky uses:
- passing
HttpServletRequestdeep into domain service, - reading arbitrary headers everywhere,
- mixing servlet API into business logic,
- relying on thread-local request objects outside request lifecycle.
Rule:
Use @Context at the resource boundary.
Do not let runtime-specific objects leak into domain/application core.
15. Return Type Choices
JAX-RS resource method can return:
- plain entity object,
Response,GenericEntity,- streaming type,
- async response type depending on runtime/support.
Plain Entity
@GET
@Path("/{quoteId}")
public QuoteDto getQuote(...) {
return quoteService.getQuote(...);
}
Pros:
- simple,
- readable,
- good for normal 200 response.
Cons:
- less explicit status/header control,
- error behavior relies on exception mapper,
- hard to set
Location,ETag, pagination headers, cache control.
Response
@POST
public Response createQuote(CreateQuoteRequest request) {
QuoteDto created = quoteService.create(request.toCommand());
return Response.status(Response.Status.CREATED)
.entity(created)
.build();
}
Pros:
- explicit status,
- explicit headers,
- supports
Location, cache, ETag, etc.
Cons:
- can become noisy,
- easy to return inconsistent envelopes/statuses if not standardized.
Senior Rule
Use plain DTO for simple read responses if team convention allows it. Use Response when HTTP semantics matter explicitly:
- create returns
201 Created, - delete returns
204 No Content, - redirect,
- conditional request,
- cache headers,
- pagination headers,
- file download,
- streaming,
- custom status.
16. Example: Production-Oriented Resource Skeleton
@Path("/quotes")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class QuoteResource {
private final QuoteApplicationService quoteService;
public QuoteResource(QuoteApplicationService quoteService) {
this.quoteService = quoteService;
}
@GET
public QuoteSearchResponse searchQuotes(@BeanParam QuoteSearchParams params,
@Context SecurityContext securityContext) {
Actor actor = Actor.from(securityContext);
QuoteSearchQuery query = params.toQuery();
return quoteService.search(actor, query);
}
@GET
@Path("/{quoteId}")
public QuoteDto getQuote(@PathParam("quoteId") String quoteId,
@Context SecurityContext securityContext) {
Actor actor = Actor.from(securityContext);
return quoteService.get(actor, QuoteId.parse(quoteId));
}
@POST
public Response createQuote(CreateQuoteRequest request,
@Context UriInfo uriInfo,
@Context SecurityContext securityContext) {
Actor actor = Actor.from(securityContext);
QuoteId quoteId = quoteService.create(actor, request.toCommand());
URI location = uriInfo.getAbsolutePathBuilder()
.path(quoteId.value())
.build();
return Response.created(location)
.entity(new CreateQuoteResponse(quoteId.value()))
.build();
}
@POST
@Path("/{quoteId}/submit")
public QuoteDto submitQuote(@PathParam("quoteId") String quoteId,
SubmitQuoteRequest request,
@Context SecurityContext securityContext) {
Actor actor = Actor.from(securityContext);
return quoteService.submit(actor, QuoteId.parse(quoteId), request.toCommand());
}
}
What this skeleton tries to preserve:
- endpoint semantics are visible,
- resource is thin,
- actor/security context is converted at boundary,
- ID parsing is explicit,
- service receives typed command/query,
- create operation uses
201 Created, - mutation uses
POSTbecause submit is a domain command.
17. Subresources and Locator Methods
JAX-RS supports subresource locators.
@Path("/quotes")
public class QuoteResource {
@Path("/{quoteId}/lines")
public QuoteLineResource lines(@PathParam("quoteId") String quoteId) {
return new QuoteLineResource(quoteId);
}
}
Then subresource handles:
public class QuoteLineResource {
private final String quoteId;
public QuoteLineResource(String quoteId) {
this.quoteId = quoteId;
}
@GET
public List<QuoteLineDto> listLines() {
...
}
}
Subresources can help when a nested resource has many operations.
But be careful:
- lifecycle/injection may be runtime-specific,
- manually constructed subresource may bypass DI,
- path flow becomes harder to trace,
- OpenAPI generation may need extra config,
- authorization checks must remain consistent.
For enterprise systems, subresource locators are powerful but should be used deliberately.
18. Validation Annotation on Resource Method
Depending on integration, Bean Validation can apply to method parameters and body DTO.
@POST
public Response createQuote(@Valid CreateQuoteRequest request) {
...
}
Parameter validation:
@GET
@Path("/{quoteId}")
public QuoteDto getQuote(@PathParam("quoteId") @NotBlank String quoteId) {
...
}
Caution:
- method validation support depends on runtime/provider setup,
- validation exception mapping must be standardized,
- path/query validation and body validation may produce different exception types,
- domain validation must not be replaced by DTO validation.
DTO validation catches transport-level invalidity. Domain validation enforces business invariants.
19. Annotation Surface and OpenAPI Generation
Many teams generate OpenAPI from resource annotations or use OpenAPI annotations alongside JAX-RS.
Example idea:
@GET
@Path("/{quoteId}")
@Produces(MediaType.APPLICATION_JSON)
public QuoteDto getQuote(@PathParam("quoteId") String quoteId) {
...
}
The generated contract may infer:
- method:
GET, - path:
/quotes/{quoteId}, - response media type:
application/json, - response schema:
QuoteDto, - parameter:
quoteId.
But inference is not always enough for:
- error responses,
- authorization requirements,
- header requirements,
- examples,
- pagination semantics,
- deprecation,
- versioning,
- tenant headers,
- idempotency key.
Internal API governance should define whether OpenAPI is:
- generated from code,
- written first then implemented,
- generated and linted,
- hybrid with manual annotations.
20. Common Failure Modes
20.1 Wrong Method Annotation
Symptom:
405 Method Not Allowed
Likely causes:
- endpoint uses
@POST, client sendsPUT, - gateway transforms method,
- CORS preflight sends
OPTIONS, runtime does not support it, - method annotation missing.
Debug:
- inspect resource class,
- inspect gateway route,
- inspect access logs,
- compare OpenAPI spec vs implementation.
20.2 Missing or Wrong @Path
Symptom:
404 Not Found
Likely causes:
- class path wrong,
- method path wrong,
- application path differs,
- servlet context path differs,
- gateway rewrites path,
- resource not registered.
Debug:
- list registered resources if runtime supports it,
- inspect
Application/ResourceConfig, - inspect deployment route,
- test internal service path directly.
20.3 Missing @Consumes
Symptom:
415 Unsupported Media Type
Likely causes:
- client sends
text/plain, method expects JSON, Content-Typemissing,- multipart provider not registered,
- custom media type unsupported.
Debug:
- check request headers,
- check resource annotation,
- check registered
MessageBodyReader, - check client library behavior.
20.4 Missing @Produces
Symptom:
406 Not Acceptable
Likely causes:
- client
Acceptdoes not match, - method does not produce requested representation,
- provider missing,
- wildcard behavior not as expected.
Debug:
- check
Acceptheader, - check
@Produces, - check provider registration,
- check whether gateway modifies
Accept.
20.5 Path/Body ID Mismatch
Symptom:
Data updated under unexpected ID
Likely causes:
- path ID and body ID both accepted,
- no consistency check,
- body mapped directly to entity,
- missing domain command boundary.
Debug:
- inspect DTO,
- inspect mapper,
- inspect service method contract,
- add negative tests.
21. Anti-Patterns
21.1 Fat Resource Method
@POST
public Response create(CreateQuoteRequest request) {
validate(request);
Connection connection = dataSource.getConnection();
// SQL
// Kafka publish
// Audit log
// Email
// PDF generation
return Response.ok().build();
}
Problems:
- no clear transaction boundary,
- hard to test,
- hard to retry,
- hard to observe,
- tight coupling to infrastructure,
- PR review becomes noisy.
21.2 Resource Returning Raw Map
return Map.of("status", "ok", "data", data);
Problems:
- weak contract,
- poor OpenAPI schema,
- no compiler support,
- compatibility hard to enforce.
21.3 Header Logic Everywhere
String tenant = headers.getHeaderString("X-Tenant-Id");
Repeated across many methods creates inconsistent tenant handling. Prefer request context/filter.
21.4 Annotation Magic Without Tests
If endpoint behavior depends on:
- custom parameter converter,
- validation integration,
- provider priority,
- OpenAPI generation,
- injection scope,
then endpoint tests must verify behavior. Annotation-only confidence is not enough.
22. PR Review Checklist
Use this checklist when reviewing JAX-RS resource methods.
HTTP Contract
- Is HTTP method semantics correct?
- Is path resource-oriented?
- Is mutation using safe/idempotent semantics correctly?
- Is
@Consumesexplicit where body exists? - Is
@Producesexplicit where response body exists? - Are status codes correct?
- Are headers required by governance included?
Input Contract
- Are path/query/header/form params named clearly?
- Are required/optional params obvious?
- Are default values safe?
- Are max limits enforced?
- Is path/body ID mismatch handled?
- Is validation at the right boundary?
Output Contract
- Is response type stable and schema-friendly?
- Are empty/null cases defined?
- Is pagination metadata consistent?
- Are error responses mapped consistently?
- Is backward compatibility preserved?
Architecture
- Is resource method thin?
- Does it delegate to application service/use case?
- Are runtime-specific objects kept at boundary?
- Does it avoid direct DB/Kafka/cache calls unless intentionally acting as adapter?
- Is tenant/security context handled consistently?
Observability and Operations
- Are correlation and request context propagated?
- Are expected errors distinguished from unexpected errors?
- Are audit/security logs triggered at correct boundary?
- Are slow or heavy endpoints measurable?
- Are file/stream endpoints protected by limits?
Testing
- Is there endpoint-level test for happy path?
- Are invalid params tested?
- Are media type errors tested?
- Are authorization negative cases tested?
- Are compatibility-critical responses tested?
23. Internal Verification Checklist
For CSG Quote & Order or any enterprise JAX-RS codebase, verify instead of assuming:
Runtime and Implementation
- Are resources standard JAX-RS only, or Jersey-specific?
- Are resources registered by package scanning or explicit registration?
- Are resource classes constructed by HK2, CDI, Spring, manual factory, or other runtime?
- Are method validation annotations active?
- Are OpenAPI docs generated from JAX-RS annotations?
API Conventions
- What is the official path naming convention?
- Is API version in path, header, media type, or gateway route?
- Which HTTP methods are allowed by platform/gateway?
- What is the standard response envelope?
- What is the standard error envelope?
- Are
PATCH,DELETE, multipart, SSE, or streaming allowed?
Headers and Context
- What headers are mandatory?
- Where is correlation ID created?
- Where is tenant resolved?
- Where is idempotency key checked?
- Where is auth token validated?
- Are these handled in filters or resource methods?
DTO and Validation
- Are DTOs generated or handwritten?
- Is Bean Validation used on request DTOs?
- Is unknown JSON field rejected or ignored?
- Are date/time/currency fields standardized?
- Are path/body ID mismatches rejected?
PR and Governance
- Is OpenAPI required for every endpoint change?
- Is API linting enabled?
- Is backward compatibility checked in CI?
- Are consumer teams notified on deprecation?
- Is there an API review board or architecture review process?
24. Practical Debug Playbook
When an endpoint does not behave as expected, debug in this order.
Step 1 — Confirm Public URL vs Internal Path
public URL -> gateway route -> ingress -> service path -> servlet path -> JAX-RS path
Do not assume @Path equals public URL.
Step 2 — Confirm HTTP Method
Check actual method in access log. Client code may not send what you think it sends.
Step 3 — Confirm Headers
Especially:
Content-Type,Accept,Authorization,- tenant header,
- correlation ID,
- idempotency key.
Step 4 — Confirm Resource Registration
If method exists but never matches, check whether resource class is registered.
Step 5 — Confirm Provider Availability
If body mapping fails, check provider:
- JSON provider,
- XML provider,
- multipart provider,
- custom reader/writer,
- object mapper config.
Step 6 — Confirm Filters and Mappers
Request may be rejected before method:
- auth filter,
- tenant filter,
- validation filter,
- rate limiter,
- exception mapper.
25. Mental Model Summary
A JAX-RS resource method is a contract boundary.
Annotation surface defines how network input maps to Java execution.
A senior engineer reads this:
@POST
@Path("/{quoteId}/submit")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public QuoteDto submitQuote(@PathParam("quoteId") String quoteId,
SubmitQuoteRequest request) {
...
}
as this:
A non-safe command endpoint under /quotes/{quoteId}/submit
accepts JSON body,
produces JSON response,
extracts quoteId from path,
deserializes request body,
invokes command logic,
then returns a quote representation or mapped error.
The method signature is not implementation detail. It is API governance, runtime behavior, and production failure surface.
26. Part 009 Learning Outcome Checklist
After this part, you should be able to:
- explain class-level and method-level
@Path, - choose HTTP method annotations based on semantics,
- use
@Consumesand@Producesdeliberately, - bind path/query/header/form/body input correctly,
- understand
@Contextwithout leaking runtime objects into domain logic, - decide when to return DTO vs
Response, - recognize resource method anti-patterns,
- review endpoint PRs with API compatibility awareness,
- debug common 404/405/406/415 issues from annotation surface,
- identify what must be verified in internal CSG runtime and governance.
You just completed lesson 09 in start here. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.
Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.