REST Runtime Mental Model
Learn Java Jakarta RESTful Web Services / JAX-RS - Part 003
Mental model runtime Jakarta REST dari HTTP request masuk sampai response keluar, termasuk resource matching, method selection, provider pipeline, filter/interceptor, exception mapping, dan failure diagnosis.
Part 003 — REST Runtime Mental Model
Banyak developer belajar Jakarta REST dari annotation:
@Path("/cases")
public class CaseResource {
@GET
public List<CaseSummaryResponse> listCases() {
return List.of();
}
}
Itu cukup untuk membuat demo. Tapi tidak cukup untuk mendesain API production.
Engineer yang kuat harus bisa membayangkan apa yang terjadi di runtime ketika request masuk:
GET /case-management/api/cases/CASE-2026-001?include=evidence HTTP/1.1
Host: enforcement.example.internal
Accept: application/json
Authorization: Bearer eyJ...
X-Correlation-Id: req-8f7a
Pertanyaan yang harus bisa dijawab:
- aplikasi Jakarta REST mana yang menerima request ini?
- root resource class mana yang cocok?
- method mana yang dipilih?
- kapan filter berjalan?
- kapan parameter diinjeksi?
- kapan body dibaca?
- provider mana yang mengubah Java object menjadi JSON?
- jika exception dilempar, siapa yang menangani?
- jika response gagal diserialisasi, status apa yang keluar?
- apakah resource instance aman untuk concurrency?
Bagian ini membangun mental model tersebut.
1. Target Pembelajaran
Setelah part ini, kita ingin bisa melakukan tiga hal:
-
Membaca request lifecycle secara deterministik
Bukan menebak “framework akan handle”, tetapi tahu urutan besar proses runtime. -
Mendiagnosis failure berdasarkan fase pipeline
404,405,406,415,400,500sering tampak seperti error biasa, padahal masing-masing biasanya berasal dari fase pipeline berbeda. -
Mendesain resource yang sesuai dengan runtime
Resource class harus ditulis sebagai adapter protokol HTTP, bukan sebagai tempat menumpuk business logic, persistence logic, dan operational concern.
2. Big Picture: Jakarta REST sebagai Runtime Boundary
Jakarta REST bukan hanya library annotation. Jakarta REST adalah boundary antara:
- HTTP request/response,
- Java object,
- application container,
- provider pipeline,
- dependency injection,
- media type negotiation,
- error handling,
- extension points.
Mental model sederhananya:
Namun diagram itu masih terlalu rapi. Dalam kasus nyata, exception bisa terjadi di hampir semua fase:
Runtime pipeline ini penting karena banyak bug API sebenarnya bukan bug business logic. Banyak bug terjadi sebelum resource method dipanggil.
Contoh:
@Pathsalah → resource method tidak pernah dipanggil.Content-Typesalah → body tidak pernah dibaca.Accepttidak cocok → method tidak dipilih.MessageBodyReadertidak tersedia → request gagal sebelum domain service dipanggil.ExceptionMapperterlalu generic → error contract menjadi tidak stabil.
3. Layer-Layer yang Terlibat
Untuk memahami Jakarta REST runtime, pisahkan empat layer ini.
| Layer | Tanggung Jawab | Contoh |
|---|---|---|
| HTTP transport/container | menerima TCP/HTTP, routing awal ke web app, servlet/filter, TLS termination bisa di luar | Servlet container, app server, embedded runtime |
| Jakarta REST application | menentukan root path aplikasi dan komponen yang tersedia | Application, @ApplicationPath |
| Resource/provider pipeline | memilih resource, inject parameter, membaca/menulis entity, filter, interceptor | @Path, @GET, MessageBodyReader, ContainerRequestFilter |
| Application/domain layer | menjalankan use case bisnis | CaseService, EvidenceService, repository, event publisher |
Boundary yang sehat:
Resource class tidak seharusnya menjadi “mini application service”. Resource class adalah adapter protokol:
- menerima input HTTP,
- memanggil use case,
- menerjemahkan result ke response HTTP,
- tidak menyimpan state mutable request antar-request,
- tidak menyembunyikan transaksi/persistence/authorization domain secara acak.
4. Request Path: Dari URL ke Resource Method
Misalkan aplikasi dideploy seperti ini:
package com.example.caseapi;
import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.core.Application;
@ApplicationPath("/api")
public class CaseApiApplication extends Application {
}
Lalu resource:
package com.example.caseapi.resource;
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;
@Path("/cases")
@Produces(MediaType.APPLICATION_JSON)
public class CaseResource {
@GET
@Path("/{caseId}")
public CaseDetailResponse getCase(@PathParam("caseId") String caseId) {
return new CaseDetailResponse(caseId, "OPEN");
}
}
Request:
GET /case-management/api/cases/CASE-2026-001 HTTP/1.1
Accept: application/json
Decompose path-nya:
| Bagian | Nilai | Pemilik |
|---|---|---|
| context path | /case-management | web application / deployment |
| application path | /api | @ApplicationPath |
| root resource path | /cases | @Path pada class |
| method path | /CASE-2026-001 | @Path("/{caseId}") pada method |
Resource method yang dipilih:
CaseResource#getCase(String caseId)
Parameter yang diinjeksi:
caseId = "CASE-2026-001"
Return value:
new CaseDetailResponse("CASE-2026-001", "OPEN")
Lalu runtime mencari MessageBodyWriter yang dapat menulis CaseDetailResponse sebagai application/json.
5. Fase 1 — Application Selection
Sebelum resource matching terjadi, runtime harus tahu aplikasi Jakarta REST mana yang aktif.
Pada deployment tradisional, ada beberapa komponen path:
/{context-path}/{application-path}/{resource-path}
Contoh:
/case-management/api/cases/CASE-2026-001
Artinya:
context-path = /case-management
application-path = /api
resource-path = /cases/CASE-2026-001
@ApplicationPath bukan resource path. Ia adalah root path untuk seluruh aplikasi Jakarta REST dalam deployment tersebut.
Contoh buruk:
@ApplicationPath("/cases")
public class CaseApiApplication extends Application {
}
Lalu:
@Path("/cases")
public class CaseResource {
}
Hasil URL menjadi:
/case-management/cases/cases
Ini sering terjadi ketika developer belum membedakan application path dan resource path.
Rule praktis:
@ApplicationPath("/api")atau@ApplicationPath("/rest")untuk boundary aplikasi.@Path("/cases"),@Path("/evidence"),@Path("/decisions")untuk resource domain.- Hindari memakai nama entity domain sebagai application path kecuali memang deployment hanya berisi satu resource family.
6. Fase 2 — Root Resource Matching
Root resource class adalah class Java yang menjadi entry point resource.
Contoh:
@Path("/cases")
public class CaseResource {
}
@Path("/evidence")
public class EvidenceResource {
}
@Path("/officers")
public class OfficerResource {
}
Runtime akan mencocokkan sisa path setelah application path.
Request:
GET /case-management/api/cases/CASE-2026-001
Sisa path untuk Jakarta REST:
/cases/CASE-2026-001
Candidate root resource:
@Path("/cases")
public class CaseResource
Bukan:
@Path("/evidence")
public class EvidenceResource
6.1 Matching Bukan Sekadar String Equality
@Path mendukung template variable:
@Path("/cases/{caseId}")
public class CaseResource {
}
Atau class-level + method-level composition:
@Path("/cases")
public class CaseResource {
@GET
@Path("/{caseId}")
public CaseDetailResponse getCase(@PathParam("caseId") String caseId) { ... }
}
Secara desain, opsi kedua biasanya lebih maintainable karena class-level path menunjukkan resource collection, method-level path menunjukkan operation pada item/subresource.
6.2 Overlapping Path
Contoh berbahaya:
@Path("/cases/{caseId}")
public class CaseByIdResource { }
@Path("/cases/search")
public class CaseSearchResource { }
Request:
GET /api/cases/search
Secara manusia, search tampak sebagai endpoint search. Secara template, search juga bisa menjadi {caseId}.
Runtime punya algoritma matching, tetapi desain yang terlalu ambigu tetap buruk. Jangan sengaja membuat URI yang bergantung pada “siapa menang matching”. Lebih baik eksplisit:
GET /api/cases?status=OPEN&assignedTo=OFF-1
GET /api/case-searches/{searchId}
POST /api/case-searches
Atau jika benar-benar command-style:
POST /api/cases/searches
7. Fase 3 — Resource Method Selection
Setelah root resource cocok, runtime memilih method berdasarkan kombinasi:
- HTTP method:
GET,POST,PUT,PATCH,DELETE,HEAD,OPTIONS. - method-level
@Path. - media type yang dikonsumsi:
@Consumes. - media type yang diproduksi:
@Produces. - request headers seperti
AcceptdanContent-Type.
Contoh:
@Path("/cases")
@Produces(MediaType.APPLICATION_JSON)
public class CaseResource {
@GET
public List<CaseSummaryResponse> listCases() { ... }
@POST
@Consumes(MediaType.APPLICATION_JSON)
public Response createCase(CreateCaseRequest request) { ... }
@GET
@Path("/{caseId}")
public CaseDetailResponse getCase(@PathParam("caseId") String caseId) { ... }
}
Request ini cocok ke listCases:
GET /api/cases
Accept: application/json
Request ini cocok ke createCase:
POST /api/cases
Content-Type: application/json
Accept: application/json
{"subject":"Unlicensed operation"}
Request ini cocok ke getCase:
GET /api/cases/CASE-2026-001
Accept: application/json
7.1 404 vs 405
Perbedaan penting:
| Status | Biasanya Berarti |
|---|---|
404 Not Found | tidak ada resource path yang cocok |
405 Method Not Allowed | path cocok, tapi HTTP method tidak tersedia |
Contoh:
@Path("/cases")
public class CaseResource {
@GET
public List<CaseSummaryResponse> listCases() { ... }
}
Request:
POST /api/cases
Path /cases cocok. Tapi method POST tidak ada. Secara mental model, ini bukan resource missing. Ini operation missing.
7.2 406 vs 415
| Status | Penyebab Umum | Header Terkait |
|---|---|---|
406 Not Acceptable | server tidak bisa menghasilkan media type yang diminta client | Accept |
415 Unsupported Media Type | server tidak bisa membaca media type body yang dikirim client | Content-Type |
Contoh:
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response createCase(CreateCaseRequest request) { ... }
Request yang memicu 415:
POST /api/cases
Content-Type: text/plain
Accept: application/json
subject=Unlicensed operation
Request yang berpotensi memicu 406:
POST /api/cases
Content-Type: application/json
Accept: application/xml
{"subject":"Unlicensed operation"}
Kesalahan umum: mengira semua error input adalah 400. Padahal mismatch media type adalah error negosiasi/protokol.
8. Fase 4 — Request Filters
ContainerRequestFilter berjalan sebelum resource method dieksekusi.
Ada dua jenis besar:
-
Pre-matching filter
Berjalan sebelum resource matching selesai. Biasanya untuk hal sangat awal seperti request normalization atau method override. Gunakan dengan sangat hati-hati. -
Post-matching filter
Berjalan setelah resource/method diketahui. Cocok untuk authorization, audit metadata, correlation context, policy enforcement.
Contoh filter correlation ID:
package com.example.caseapi.infrastructure.http;
import java.io.IOException;
import java.util.UUID;
import jakarta.annotation.Priority;
import jakarta.ws.rs.Priorities;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerRequestFilter;
import jakarta.ws.rs.container.ContainerResponseContext;
import jakarta.ws.rs.container.ContainerResponseFilter;
import jakarta.ws.rs.ext.Provider;
@Provider
@Priority(Priorities.HEADER_DECORATOR)
public class CorrelationIdFilter implements ContainerRequestFilter, ContainerResponseFilter {
public static final String HEADER = "X-Correlation-Id";
public static final String PROPERTY = "correlationId";
@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
String correlationId = requestContext.getHeaderString(HEADER);
if (correlationId == null || correlationId.isBlank()) {
correlationId = UUID.randomUUID().toString();
}
requestContext.setProperty(PROPERTY, correlationId);
}
@Override
public void filter(ContainerRequestContext requestContext,
ContainerResponseContext responseContext) throws IOException {
Object correlationId = requestContext.getProperty(PROPERTY);
if (correlationId != null) {
responseContext.getHeaders().putSingle(HEADER, correlationId.toString());
}
}
}
Mental model:
Request filter bisa menghentikan pipeline:
requestContext.abortWith(
Response.status(Response.Status.UNAUTHORIZED)
.entity(new ErrorResponse("AUTHENTICATION_REQUIRED"))
.build()
);
Ini penting. Jika filter melakukan abortWith, resource method tidak dipanggil.
Anti-pattern:
@Provider
public class BusinessRuleFilter implements ContainerRequestFilter {
@Override
public void filter(ContainerRequestContext ctx) {
// Jangan taruh business process besar di sini.
// Filter adalah cross-cutting boundary, bukan use-case layer.
}
}
Filter cocok untuk:
- correlation ID,
- authentication metadata extraction,
- coarse authorization boundary,
- request size policy,
- audit envelope,
- header normalization,
- tenant context extraction.
Filter buruk untuk:
- create case,
- approve decision,
- mutate evidence,
- orchestrate workflow,
- call many repositories,
- hide domain branching.
9. Fase 5 — Parameter Injection
Setelah method dipilih, runtime mengisi parameter dari berbagai sumber:
@GET
@Path("/{caseId}/events")
public List<CaseEventResponse> listEvents(
@PathParam("caseId") String caseId,
@QueryParam("type") String type,
@HeaderParam("X-Tenant-Id") String tenantId,
@Context jakarta.ws.rs.core.UriInfo uriInfo
) {
...
}
Sumber parameter:
| Annotation | Sumber |
|---|---|
@PathParam | path template |
@QueryParam | query string |
@HeaderParam | HTTP header |
@CookieParam | cookie |
@MatrixParam | matrix parameter path segment |
@FormParam | form body |
@BeanParam | grouping beberapa parameter |
@Context | runtime context object |
9.1 Parameter Conversion
Jika parameter bertipe non-String, runtime akan mencoba konversi.
@GET
public List<CaseSummaryResponse> listCases(
@QueryParam("limit") @DefaultValue("50") int limit
) {
...
}
Request valid:
GET /api/cases?limit=25
Request bermasalah:
GET /api/cases?limit=abc
Ini gagal sebelum business logic berjalan. Jangan tangani ini dengan try/catch di resource method karena method tidak akan dipanggil jika conversion gagal.
9.2 Jangan Menjadikan Parameter Injection sebagai Domain Validation
Contoh buruk:
@GET
public List<CaseSummaryResponse> listCases(
@QueryParam("status") String status,
@QueryParam("assignedTo") String assignedTo,
@QueryParam("createdAfter") String createdAfter
) {
if (!List.of("OPEN", "CLOSED").contains(status)) {
throw new BadRequestException();
}
...
}
Lebih baik buat boundary object:
public class ListCasesQuery {
@QueryParam("status")
private String status;
@QueryParam("assignedTo")
private String assignedTo;
@QueryParam("createdAfter")
private String createdAfter;
public CaseSearchCriteria toCriteria() {
return CaseSearchCriteria.builder()
.status(CaseStatus.parse(status))
.assignedTo(assignedTo)
.createdAfter(DateParsers.parseInstant(createdAfter))
.build();
}
}
Lalu:
@GET
public List<CaseSummaryResponse> listCases(@BeanParam ListCasesQuery query) {
return service.listCases(query.toCriteria());
}
@BeanParam membuat method signature lebih stabil dan lebih mudah berkembang.
10. Fase 6 — Entity Body Reading
Untuk method yang menerima body:
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response createCase(CreateCaseRequest request) {
...
}
Runtime harus melakukan:
HTTP body bytes -> Java object CreateCaseRequest
Yang melakukan ini adalah MessageBodyReader.
Secara mental model:
Jika Content-Type: application/json, runtime mencari provider yang mampu membaca JSON menjadi CreateCaseRequest.
Jika provider tidak ada, atau body tidak valid, method tidak akan berjalan.
10.1 Kapan Body Dibaca?
Body dibaca sebelum resource method menerima parameter entity.
Contoh:
public Response createCase(CreateCaseRequest request) { ... }
Parameter request sudah berupa object ketika method dipanggil. Jika JSON malformed, method tidak dipanggil.
10.2 DTO Harus Menjadi Boundary, Bukan Entity Persistence
Buruk:
@POST
public Response createCase(CaseEntity entity) {
repository.persist(entity);
return Response.ok().build();
}
Masalah:
- field persistence bocor ke API,
- client bisa mengisi field internal,
- validasi tidak eksplisit,
- perubahan database menjadi breaking API change,
- serialization annotation bercampur persistence annotation,
- audit trail sulit dikontrol.
Lebih baik:
public record CreateCaseRequest(
String subject,
String allegationType,
String reportedBy,
String narrative
) {}
Lalu mapping ke command:
CreateCaseCommand command = new CreateCaseCommand(
request.subject(),
request.allegationType(),
request.reportedBy(),
request.narrative(),
authenticatedOfficerId
);
11. Fase 7 — Resource Method Execution
Resource method adalah titik transisi dari protocol world ke application world.
Contoh sehat:
@Path("/cases")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class CaseResource {
private final CaseApplicationService service;
public CaseResource(CaseApplicationService service) {
this.service = service;
}
@POST
public Response createCase(CreateCaseRequest request, @Context UriInfo uriInfo) {
CreatedCase result = service.createCase(request.toCommand());
URI location = uriInfo.getAbsolutePathBuilder()
.path(result.caseId())
.build();
return Response.created(location)
.entity(CreatedCaseResponse.from(result))
.build();
}
}
Resource method melakukan:
- mengambil DTO request,
- mengambil context HTTP yang diperlukan,
- memanggil application service,
- membangun response HTTP.
Resource method tidak melakukan:
- query SQL langsung,
- transaksi manual yang tersebar,
- parsing JSON manual,
- audit persistence manual tanpa abstraction,
- retry outbound call secara tersembunyi,
- mengubah global state.
11.1 Resource Lifecycle dan State
Secara default, resource class instance dibuat per request. Namun runtime/container/CDI dapat memperkenalkan lifecycle berbeda, misalnya singleton provider, CDI scoped bean, atau object dari Application#getSingletons().
Karena itu, rule production yang aman:
- anggap provider/filter/interceptor bisa dipakai lintas request,
- jangan simpan request-specific mutable state di field instance,
- gunakan local variable atau request context,
- gunakan dependency yang thread-safe atau scoped dengan benar,
- jangan mengandalkan constructor untuk semua injection jika portabilitas lintas runtime penting.
Buruk:
@Path("/cases")
public class CaseResource {
private String currentCaseId; // bahaya jika lifecycle berubah
@GET
@Path("/{caseId}")
public CaseDetailResponse getCase(@PathParam("caseId") String caseId) {
this.currentCaseId = caseId;
return service.getCase(currentCaseId);
}
}
Baik:
@GET
@Path("/{caseId}")
public CaseDetailResponse getCase(@PathParam("caseId") String caseId) {
return service.getCase(caseId);
}
12. Fase 8 — Response Building
Resource method bisa return beberapa bentuk:
public CaseDetailResponse getCase(...) { ... }
public Response createCase(...) { ... }
public void deleteCase(...) { ... }
Untuk API production, Response sering lebih eksplisit karena kita perlu mengontrol:
- status code,
- headers,
- entity,
Location,ETag,- cache headers,
- content negotiation behavior,
- no-content response.
Contoh:
return Response.status(Response.Status.ACCEPTED)
.header("X-Correlation-Id", correlationId)
.entity(new AcceptedCommandResponse(commandId))
.build();
Namun jangan overuse Response jika method hanya query sederhana dan tidak butuh metadata HTTP khusus.
@GET
@Path("/{caseId}")
public CaseDetailResponse getCase(@PathParam("caseId") String caseId) {
return CaseDetailResponse.from(service.getCase(caseId));
}
Prinsip:
- Query sederhana boleh return DTO.
- Mutation/creation/deletion lebih baik return
Responseagar status dan header eksplisit. - Error jangan dibangun tersebar di semua method; gunakan exception mapper.
13. Fase 9 — Entity Body Writing
Setelah resource method return entity, runtime harus melakukan:
Java object -> response bytes
Yang melakukan ini adalah MessageBodyWriter.
Jika method return:
public CaseDetailResponse getCase(...) { ... }
Dan response media type adalah:
application/json
Runtime mencari writer untuk:
CaseDetailResponse + application/json
Jika tidak ada writer, request bisa gagal setelah business logic berhasil. Ini menyakitkan karena domain action mungkin sudah terjadi, tetapi response gagal ditulis.
Karena itu mutation endpoint harus didesain hati-hati:
@POST
public Response createCase(CreateCaseRequest request) {
CreatedCase created = service.createCase(...);
return Response.created(...)
.entity(CreatedCaseResponse.from(created))
.build();
}
Pastikan CreatedCaseResponse benar-benar serializable oleh provider JSON yang dipakai.
13.1 Serialization Failure Adalah Production Risk
Contoh response object yang bermasalah:
public class CaseDetailResponse {
private final String caseId;
private final Instant createdAt;
public CaseDetailResponse(String caseId, Instant createdAt) {
this.caseId = caseId;
this.createdAt = createdAt;
}
// tidak ada accessor yang dikenali provider tertentu
}
Tergantung provider JSON, ini bisa gagal atau menghasilkan {}.
Lebih stabil:
public record CaseDetailResponse(
String caseId,
String status,
Instant createdAt
) {}
Tapi tetap cek provider JSON dan konfigurasi time serialization.
14. Fase 10 — Response Filters
ContainerResponseFilter berjalan setelah resource method menghasilkan response, tetapi sebelum response dikirim.
Cocok untuk:
- correlation ID response header,
- security headers,
- cache policy default,
- audit envelope,
- metrics tagging,
- deprecation warning header,
- API version metadata.
Contoh security header filter:
@Provider
public class SecurityHeaderFilter implements ContainerResponseFilter {
@Override
public void filter(ContainerRequestContext requestContext,
ContainerResponseContext responseContext) {
responseContext.getHeaders().putSingle("X-Content-Type-Options", "nosniff");
responseContext.getHeaders().putSingle("X-Frame-Options", "DENY");
}
}
Jangan gunakan response filter untuk mengubah hasil domain secara besar-besaran. Jika response filter perlu tahu detail business state terlalu banyak, kemungkinan concern-nya salah tempat.
15. Exception Flow
Exception bisa terjadi pada fase mana pun:
- resource matching,
- parameter conversion,
- entity reading,
- resource method,
- entity writing,
- filter/interceptor.
Jakarta REST menyediakan ExceptionMapper<T> untuk memetakan exception menjadi response.
Contoh:
@Provider
public class CaseNotFoundMapper implements ExceptionMapper<CaseNotFoundException> {
@Override
public Response toResponse(CaseNotFoundException exception) {
return Response.status(Response.Status.NOT_FOUND)
.entity(new ProblemResponse(
"CASE_NOT_FOUND",
"Case was not found",
exception.caseId()
))
.type(MediaType.APPLICATION_JSON)
.build();
}
}
Mental model:
Top-tier design tidak hanya bertanya “exception apa yang dilempar?”, tetapi:
- apakah exception ini domain error atau platform error?
- apakah client boleh melihat detailnya?
- apakah status code stabil?
- apakah error code machine-readable?
- apakah correlation ID tersedia?
- apakah response shape konsisten?
- apakah mapper terlalu generic?
15.1 Jangan Menangkap Semua Error di Resource Method
Buruk:
@POST
public Response createCase(CreateCaseRequest request) {
try {
return Response.ok(service.createCase(request.toCommand())).build();
} catch (Exception e) {
return Response.serverError().build();
}
}
Masalah:
- error contract tersebar,
- observability buruk,
- domain exception kehilangan makna,
- client tidak bisa membedakan validation/domain/conflict/not-found,
- status code menjadi kasar.
Lebih baik:
@POST
public Response createCase(CreateCaseRequest request) {
CreatedCase created = service.createCase(request.toCommand());
return Response.created(locationFor(created)).entity(CreatedCaseResponse.from(created)).build();
}
Lalu mapper:
@Provider
public class DomainExceptionMapper implements ExceptionMapper<DomainException> {
@Override
public Response toResponse(DomainException exception) {
return Response.status(mapStatus(exception))
.entity(ProblemResponse.from(exception))
.type(MediaType.APPLICATION_JSON)
.build();
}
}
16. Interceptors vs Filters
Filter bekerja pada request/response context. Interceptor bekerja lebih dekat ke entity stream.
| Komponen | Fokus | Contoh Use Case |
|---|---|---|
ContainerRequestFilter | request metadata sebelum method | auth, correlation, tenant |
ContainerResponseFilter | response metadata sebelum dikirim | security header, cache header |
ReaderInterceptor | proses pembacaan entity body | decrypt, decompress, audit raw body terbatas |
WriterInterceptor | proses penulisan entity body | compress, sign response, wrap stream |
Mental model:
Rule praktis:
- Gunakan filter untuk header/context/policy.
- Gunakan interceptor untuk entity stream concern.
- Jangan gunakan interceptor untuk business logic.
- Hindari membaca body di request filter kecuali benar-benar paham konsekuensinya terhadap stream consumption.
17. Request Lifecycle Walkthrough: Create Case
Kita gunakan endpoint realistic.
@Path("/cases")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class CaseResource {
private final CaseApplicationService service;
public CaseResource(CaseApplicationService service) {
this.service = service;
}
@POST
public Response createCase(CreateCaseRequest request, @Context UriInfo uriInfo) {
CreatedCase created = service.createCase(request.toCommand());
URI location = uriInfo.getAbsolutePathBuilder()
.path(created.caseId())
.build();
return Response.created(location)
.entity(CreatedCaseResponse.from(created))
.build();
}
}
Request:
POST /case-management/api/cases HTTP/1.1
Content-Type: application/json
Accept: application/json
X-Correlation-Id: req-123
{
"subject": "Unlicensed activity",
"allegationType": "LICENSE_VIOLATION",
"reportedBy": "OFFICER-17",
"narrative": "Observed repeated activity without valid license."
}
Runtime path:
- Container menerima HTTP request.
- Context path
/case-managementmenentukan web app. @ApplicationPath("/api")menentukan Jakarta REST application.@Path("/cases")menentukan root resource.@POSTmenentukan resource method.Content-Type: application/jsondicocokkan dengan@Consumes.Accept: application/jsondicocokkan dengan@Produces.- Request filter mengambil/menetapkan correlation ID.
MessageBodyReadermembaca JSON menjadiCreateCaseRequest.UriInfodiinjeksi.- Resource method memanggil
CaseApplicationService. - Service membuat case dan mengembalikan result.
- Resource method membangun
201 Created+Location. MessageBodyWritermenulisCreatedCaseResponsemenjadi JSON.- Response filter menambahkan header.
- Client menerima response.
Response:
HTTP/1.1 201 Created
Content-Type: application/json
Location: /case-management/api/cases/CASE-2026-001
X-Correlation-Id: req-123
{
"caseId": "CASE-2026-001",
"status": "OPEN"
}
18. Debugging by Pipeline Phase
Ketika endpoint gagal, jangan langsung buka service method. Tanyakan dulu: request gagal di fase mana?
| Symptom | Kemungkinan Fase | Hal yang Dicek |
|---|---|---|
404 | application/resource matching | context path, @ApplicationPath, class @Path, method @Path |
405 | method selection | ada @GET/@POST yang sesuai? |
406 | response negotiation | Accept, @Produces, writer tersedia? |
415 | request entity negotiation | Content-Type, @Consumes, reader tersedia? |
400 sebelum method | parameter/entity conversion | path/query conversion, malformed JSON |
401/403 | filter/security layer | auth filter, role mapping, SecurityContext |
500 setelah service sukses | response serialization | DTO shape, JSON provider, circular reference |
| response header hilang | response filter | provider discovery, priority, name binding |
| method tidak kena log | request tidak mencapai resource | pre-matching filter, servlet mapping, path |
Diagnosis yang baik dimulai dari path decomposition:
Full URL
= scheme + host + port
+ context path
+ application path
+ resource path
+ method path
+ query
Lalu cek negotiation:
Request body? -> Content-Type + @Consumes + MessageBodyReader
Response body? -> Accept + @Produces + MessageBodyWriter
Lalu cek execution:
Filters -> Parameter Injection -> Entity Read -> Resource Method -> Entity Write -> Response Filters
19. Production Invariants
Invariants yang harus dijaga saat menulis Jakarta REST resource:
19.1 Resource Method Harus Jelas Side Effect-nya
GET tidak boleh melakukan mutation domain.
Buruk:
@GET
@Path("/{caseId}/mark-viewed")
public Response markViewed(@PathParam("caseId") String caseId) { ... }
Lebih baik:
@POST
@Path("/{caseId}/view-events")
public Response recordView(@PathParam("caseId") String caseId) { ... }
Atau jika hanya analytics non-critical, jangan campurkan dengan domain state yang authoritative.
19.2 Resource Tidak Menyimpan Request State di Field
Gunakan local variable, context, atau request-scoped dependency.
19.3 Provider Global Harus Minimal dan Terprediksi
Provider global bisa mempengaruhi seluruh API. Jangan membuat provider yang diam-diam mengubah kontrak banyak endpoint.
19.4 Exception Mapper Harus Stabil
Error response adalah bagian dari API contract. Jangan membocorkan stack trace, SQL error, nama table, atau detail internal.
19.5 Serialization Harus Dites
Test tidak cukup memanggil method Java. Test harus melewati runtime minimal untuk memastikan provider JSON, status, header, dan body benar.
20. Mental Model untuk Regulatory / Case Management API
Dalam sistem enforcement atau case management, Jakarta REST resource sering menjadi boundary untuk aksi yang harus bisa dipertanggungjawabkan.
Contoh endpoint:
POST /api/cases/{caseId}/escalations
POST /api/cases/{caseId}/evidence
POST /api/cases/{caseId}/decisions
POST /api/cases/{caseId}/state-transitions
Setiap mutation harus punya model:
Pertanyaan production:
- apakah actor identity tersedia sebelum use case?
- apakah idempotency diperlukan?
- apakah request body menjadi evidence?
- apakah failure response bisa diaudit?
- apakah transition invalid menghasilkan
409 Conflictatau422 Unprocessable Contentstyle response? - apakah correlation ID mengikat log, audit event, dan outbound call?
Jakarta REST hanya memberi pipeline. Desain defensibility tetap tanggung jawab aplikasi.
21. Common Pitfalls
21.1 Menganggap Annotation Adalah Arsitektur
Annotation hanya mapping. Arsitektur ada pada boundary, contract, lifecycle, dan failure model.
21.2 Menaruh Business Logic di Filter
Filter menggoda karena berjalan otomatis. Tapi justru karena otomatis, filter harus dipakai untuk cross-cutting concern yang benar-benar cross-cutting.
21.3 Mengembalikan Persistence Entity
Ini mencampur API contract dengan storage model.
21.4 Tidak Mengontrol Media Type
Endpoint tanpa @Produces/@Consumes yang jelas sering menghasilkan behavior yang berbeda antar-runtime atau antar-provider.
21.5 Tidak Memahami 404 vs 405 vs 406 vs 415
Semua tampak seperti “endpoint error”, padahal masing-masing menunjukkan fase berbeda.
21.6 Mengandalkan Runtime-Specific Magic
Beberapa implementation memberi fitur tambahan. Boleh digunakan, tapi isolasi agar tidak merusak portabilitas.
22. Practice: Trace Request Lifecycle
Ambil endpoint ini:
@Path("/cases")
@Produces(MediaType.APPLICATION_JSON)
public class CaseResource {
@GET
@Path("/{caseId}/evidence")
public List<EvidenceResponse> listEvidence(
@PathParam("caseId") String caseId,
@QueryParam("type") String type
) {
return service.listEvidence(caseId, type);
}
}
Trace request ini:
GET /case-management/api/cases/CASE-1/evidence?type=PHOTO
Accept: application/json
Jawaban yang harus bisa dibuat:
context path = /case-management
application path = /api
root resource = /cases
method path = /{caseId}/evidence
caseId = CASE-1
query type = PHOTO
http method = GET
produces = application/json
reader needed? = no request entity
writer needed? = yes, List<EvidenceResponse> -> JSON
Lalu ubah request:
POST /case-management/api/cases/CASE-1/evidence?type=PHOTO
Accept: application/json
Prediksi:
Path likely matches, method does not -> 405 Method Not Allowed
Lalu ubah:
GET /case-management/api/cases/CASE-1/evidence?type=PHOTO
Accept: application/xml
Prediksi:
Resource/method matches, but response media type may not -> 406 Not Acceptable
Latihan seperti ini membangun kemampuan diagnosis yang jauh lebih bernilai daripada menghafal annotation.
23. Ringkasan
Jakarta REST runtime bisa dipahami sebagai pipeline:
HTTP request
-> container/application selection
-> resource matching
-> method selection
-> filters
-> parameter injection
-> entity reading
-> resource method
-> response building
-> entity writing
-> response filters
-> HTTP response
Mental model utama:
@ApplicationPathmenentukan root aplikasi REST.@Pathpada class/method menentukan resource matching.- HTTP method annotation menentukan operation selection.
@ConsumesdanContent-Typemenentukan cara membaca request body.@ProducesdanAcceptmenentukan cara menulis response body.- Filter menangani cross-cutting request/response concern.
- Interceptor menangani entity stream concern.
MessageBodyReaderdanMessageBodyWritermengubah bytes ↔ Java object.ExceptionMappermengubah exception menjadi response contract.
Jika kita memahami pipeline ini, kita bisa mendesain endpoint secara sadar, bukan hanya berharap runtime melakukan hal yang benar.
24. Checklist Cepat
Sebelum menulis endpoint baru, jawab:
- Apa full URL decomposition-nya?
- Resource collection/item apa yang direpresentasikan?
- HTTP method sudah benar secara semantik?
- Request body perlu
@Consumesapa? - Response perlu
@Producesapa? - Status code sukses apa yang benar?
- Error apa saja yang mungkin dan mapper mana yang menangani?
- Apakah filter/interceptor yang ada akan mempengaruhi endpoint ini?
- Apakah DTO aman untuk serialization/deserialization?
- Apakah mutation perlu idempotency key?
- Apakah observability/correlation/audit sudah tersedia?
25. Referensi Resmi
- Jakarta RESTful Web Services 4.0 Specification:
https://jakarta.ee/specifications/restful-ws/4.0/jakarta-restful-ws-spec-4.0 - Jakarta EE Tutorial — Building RESTful Web Services with Jakarta REST:
https://jakarta.ee/learn/docs/jakartaee-tutorial/current/websvcs/rest/rest.html - Jakarta RESTful Web Services Explained:
https://jakarta.ee/learn/specification-guides/restful-web-services-explained/
You just completed lesson 03 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.