11# customer-service-client
22
33Generated Java client for the demo ** customer-service** , showcasing ** type-safe generic responses** with OpenAPI + a
4- custom Mustache template (wrapping payloads in a reusable ` ApiClientResponse <T>` ).
4+ custom Mustache template (wrapping payloads in a reusable ` ServiceClientResponse <T>` ).
55
66This module demonstrates how to evolve OpenAPI Generator with minimal customization to support generic response
77envelopes — avoiding duplicated wrappers and preserving strong typing.
@@ -11,8 +11,8 @@ envelopes — avoiding duplicated wrappers and preserving strong typing.
1111## ✅ What You Get
1212
1313* Generated code using ** OpenAPI Generator** (` restclient ` with Spring Framework ` RestClient ` ).
14- * A reusable generic base: ` io.github.bsayli.openapi.client.common.ApiClientResponse <T> ` .
15- * Thin wrappers per endpoint (e.g. ` ApiResponseCustomerCreateResponse ` , ` ApiResponseCustomerUpdateResponse ` ).
14+ * A reusable generic base: ` io.github.bsayli.openapi.client.common.ServiceClientResponse <T> ` .
15+ * Thin wrappers per endpoint (e.g. ` ServiceResponseCustomerCreateResponse ` , ` ServiceResponseCustomerUpdateResponse ` ).
1616* Spring Boot configuration to auto-expose the client as beans.
1717* Focused integration tests using ** OkHttp MockWebServer** covering all CRUD endpoints.
1818
@@ -57,28 +57,29 @@ target/generated-sources/openapi/src/gen/java/main
5757Include this module as a dependency and configure the base URL:
5858
5959``` java
60+
6061@Configuration
6162public class CustomerApiClientConfig {
6263
63- @Bean
64- public RestClient customerRestClient (RestClient .Builder builder ,
65- @Value (" ${customer.api.base-url}" ) String baseUrl ) {
66- return builder. baseUrl(baseUrl). build();
67- }
68-
69- @Bean
70- public io.github.bsayli.openapi.client.generated.invoker. ApiClient customerApiClient (
71- RestClient customerRestClient ,
72- @Value (" ${customer.api.base-url}" ) String baseUrl ) {
73- return new io.github.bsayli.openapi.client.generated.invoker. ApiClient (customerRestClient)
74- .setBasePath(baseUrl);
75- }
76-
77- @Bean
78- public io.github.bsayli.openapi.client.generated.api. CustomerControllerApi customerControllerApi (
79- io.github.bsayli.openapi.client.generated.invoker. ApiClient apiClient ) {
80- return new io.github.bsayli.openapi.client.generated.api. CustomerControllerApi (apiClient);
81- }
64+ @Bean
65+ public RestClient customerRestClient (RestClient .Builder builder ,
66+ @Value (" ${customer.api.base-url}" ) String baseUrl ) {
67+ return builder. baseUrl(baseUrl). build();
68+ }
69+
70+ @Bean
71+ public io.github.bsayli.openapi.client.generated.invoker. ApiClient customerApiClient (
72+ RestClient customerRestClient ,
73+ @Value (" ${customer.api.base-url}" ) String baseUrl ) {
74+ return new io.github.bsayli.openapi.client.generated.invoker. ApiClient (customerRestClient)
75+ .setBasePath(baseUrl);
76+ }
77+
78+ @Bean
79+ public io.github.bsayli.openapi.client.generated.api. CustomerControllerApi customerControllerApi (
80+ io.github.bsayli.openapi.client.generated.invoker. ApiClient apiClient ) {
81+ return new io.github.bsayli.openapi.client.generated.api. CustomerControllerApi (apiClient);
82+ }
8283}
8384```
8485
@@ -91,27 +92,93 @@ customer.api.base-url=http://localhost:8084/customer-service
9192** Usage example:**
9293
9394``` java
95+
9496@Autowired
9597private io.github.bsayli.openapi.client.generated.api. CustomerControllerApi customerApi;
9698
9799public void createCustomer() {
98- var req = new io.github.bsayli.openapi.client.generated.dto. CustomerCreateRequest ()
99- .name(" Jane Doe" )
100- 100+ var req = new io.github.bsayli.openapi.client.generated.dto. CustomerCreateRequest ()
101+ .name(" Jane Doe" )
102+ 103+
104+ var resp = customerApi. createCustomer(req); // ServiceResponseCustomerCreateResponse
105+
106+ System . out. println(resp. getStatus()); // 201
107+ System . out. println(resp. getData(). getCustomer(). getName()); // "Jane Doe"
108+ }
109+ ```
110+
111+ ---
112+
113+ ### Option A.2 — Alternative with HttpClient5 (connection pooling)
114+
115+ If you want more control (connection pooling, timeouts, etc.), you can wire the client with ** Apache HttpClient5** :
101116
102- var resp = customerApi. createCustomer(req); // ApiResponseCustomerCreateResponse
117+ ``` java
118+
119+ @Configuration
120+ public class CustomerApiClientConfig {
121+
122+ @Bean (destroyMethod = " close" )
123+ CloseableHttpClient customerHttpClient (
124+ @Value (" ${customer.api.max-connections-total:64}" ) int maxTotal ,
125+ @Value (" ${customer.api.max-connections-per-route:16}" ) int maxPerRoute ) {
126+
127+ var cm = PoolingHttpClientConnectionManagerBuilder . create()
128+ .setMaxConnTotal(maxTotal)
129+ .setMaxConnPerRoute(maxPerRoute)
130+ .build();
131+
132+ return HttpClients . custom()
133+ .setConnectionManager(cm)
134+ .evictExpiredConnections()
135+ .evictIdleConnections(org.apache.hc.core5.util. TimeValue . ofSeconds(30 ))
136+ .setUserAgent(" customer-service-client" )
137+ .disableAutomaticRetries()
138+ .build();
139+ }
140+
141+ @Bean
142+ HttpComponentsClientHttpRequestFactory customerRequestFactory (
143+ CloseableHttpClient customerHttpClient ,
144+ @Value (" ${customer.api.connect-timeout-seconds:10}" ) long connect ,
145+ @Value (" ${customer.api.connection-request-timeout-seconds:10}" ) long connReq ,
146+ @Value (" ${customer.api.read-timeout-seconds:15}" ) long read ) {
147+
148+ var f = new HttpComponentsClientHttpRequestFactory (customerHttpClient);
149+ f. setConnectTimeout(Duration . ofSeconds(connect));
150+ f. setConnectionRequestTimeout(Duration . ofSeconds(connReq));
151+ f. setReadTimeout(Duration . ofSeconds(read));
152+ return f;
153+ }
103154
104- System . out. println(resp. getStatus()); // 201
105- System . out. println(resp. getData(). getCustomer(). getName()); // "Jane Doe"
155+ @Bean
156+ RestClient customerRestClient (RestClient .Builder builder ,
157+ HttpComponentsClientHttpRequestFactory rf ) {
158+ return builder. requestFactory(rf). build();
159+ }
160+
161+ @Bean
162+ ApiClient customerApiClient (RestClient restClient ,
163+ @Value (" ${customer.api.base-url}" ) String baseUrl ) {
164+ return new ApiClient (restClient). setBasePath(baseUrl);
165+ }
166+
167+ @Bean
168+ CustomerControllerApi customerControllerApi (ApiClient apiClient ) {
169+ return new CustomerControllerApi (apiClient);
170+ }
106171}
107172```
108173
174+ ---
175+
109176### Option B — Manual Wiring (no Spring context)
110177
111178``` java
112179var rest = RestClient . builder(). baseUrl(" http://localhost:8084/customer-service" ). build();
113180var apiClient = new io.github.bsayli.openapi.client.generated.invoker. ApiClient (rest)
114- .setBasePath(" http://localhost:8084/customer-service" );
181+ .setBasePath(" http://localhost:8084/customer-service" );
115182var customerApi = new io.github.bsayli.openapi.client.generated.api. CustomerControllerApi (apiClient);
116183```
117184
@@ -125,7 +192,7 @@ For larger applications, encapsulate the generated API in an adapter:
125192package io.github.bsayli.openapi.client.adapter.impl ;
126193
127194import io.github.bsayli.openapi.client.adapter.CustomerClientAdapter ;
128- import io.github.bsayli.openapi.client.common.ApiClientResponse ;
195+ import io.github.bsayli.openapi.client.common.ServiceClientResponse ;
129196import io.github.bsayli.openapi.client.generated.api.CustomerControllerApi ;
130197import io.github.bsayli.openapi.client.generated.dto.* ;
131198import org.springframework.stereotype.Service ;
@@ -140,27 +207,27 @@ public class CustomerClientAdapterImpl implements CustomerClientAdapter {
140207 }
141208
142209 @Override
143- public ApiClientResponse <CustomerCreateResponse > createCustomer (CustomerCreateRequest request ) {
210+ public ServiceClientResponse <CustomerCreateResponse > createCustomer (CustomerCreateRequest request ) {
144211 return customerControllerApi. createCustomer(request);
145212 }
146213
147214 @Override
148- public ApiClientResponse <CustomerDto > getCustomer (Integer customerId ) {
215+ public ServiceClientResponse <CustomerDto > getCustomer (Integer customerId ) {
149216 return customerControllerApi. getCustomer(customerId);
150217 }
151218
152219 @Override
153- public ApiClientResponse <CustomerListResponse > getCustomers () {
220+ public ServiceClientResponse <CustomerListResponse > getCustomers () {
154221 return customerControllerApi. getCustomers();
155222 }
156223
157224 @Override
158- public ApiClientResponse <CustomerUpdateResponse > updateCustomer (Integer customerId , CustomerUpdateRequest request ) {
225+ public ServiceClientResponse <CustomerUpdateResponse > updateCustomer (Integer customerId , CustomerUpdateRequest request ) {
159226 return customerControllerApi. updateCustomer(customerId, request);
160227 }
161228
162229 @Override
163- public ApiClientResponse <CustomerDeleteResponse > deleteCustomer (Integer customerId ) {
230+ public ServiceClientResponse <CustomerDeleteResponse > deleteCustomer (Integer customerId ) {
164231 return customerControllerApi. deleteCustomer(customerId);
165232 }
166233}
@@ -170,7 +237,8 @@ This ensures:
170237
171238* Generated code stays isolated.
172239* Business code depends only on the adapter interface.
173- * Naming conventions are consistent with the service (createCustomer, getCustomer, getCustomers, updateCustomer, deleteCustomer).
240+ * Naming conventions are consistent with the service (createCustomer, getCustomer, getCustomers, updateCustomer,
241+ deleteCustomer).
174242
175243---
176244
@@ -179,11 +247,11 @@ This ensures:
179247The template at ` src/main/resources/openapi-templates/api_wrapper.mustache ` emits wrappers like:
180248
181249``` java
182- import io.github.bsayli.openapi.client.common.ApiClientResponse ;
250+ import io.github.bsayli.openapi.client.common.ServiceClientResponse ;
183251
184- // e.g., ApiResponseCustomerCreateResponse
185- public class ApiResponseCustomerCreateResponse
186- extends ApiClientResponse <CustomerCreateResponse > {
252+ // e.g., ServiceResponseCustomerCreateResponse
253+ public class ServiceResponseCustomerCreateResponse
254+ extends ServiceClientResponse <CustomerCreateResponse > {
187255}
188256```
189257
@@ -199,13 +267,15 @@ Integration test with MockWebServer:
199267mvn -q -DskipITs=false test
200268```
201269
202- It enqueues responses for ** all CRUD operations** and asserts correct mapping into the respective wrappers (e.g. ` ApiResponseCustomerCreateResponse ` , ` ApiResponseCustomerUpdateResponse ` ).
270+ It enqueues responses for ** all CRUD operations** and asserts correct mapping into the respective wrappers (e.g.
271+ ` ServiceResponseCustomerCreateResponse ` , ` ServiceResponseCustomerUpdateResponse ` ).
203272
204273---
205274
206275## 📚 Notes
207276
208- * Dependencies like ` spring-web ` , ` spring-context ` , ` jackson-* ` , ` jakarta.* ` are marked ** provided** ; your host app supplies them.
277+ * Dependencies like ` spring-web ` , ` spring-context ` , ` jackson-* ` , ` jakarta.* ` are marked ** provided** ; your host app
278+ supplies them.
209279* Generator options: Spring 6 ` RestClient ` , Jakarta EE, Jackson, Java 21.
210280* OpenAPI spec path:
211281
0 commit comments