Skip to content

Commit 082288e

Browse files
committed
chore(release): bump version to 0.7.0 for response modernization
1 parent f2e9578 commit 082288e

File tree

9 files changed

+337
-227
lines changed

9 files changed

+337
-227
lines changed

README.md

Lines changed: 46 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,11 @@
1616
<em>End-to-end generics-aware OpenAPI clients — unified <code>{ data, meta }</code> responses without boilerplate.</em>
1717
</p>
1818

19-
**Modern, type-safe OpenAPI client generation** — powered by **Spring Boot 3.4**, **Java 21**, and **OpenAPI Generator 7.16.0**.
20-
This repository demonstrates a production-grade architecture where backend and client are fully aligned through generics, enabling nested generic envelopes (ServiceResponse<Page<T>>) and RFC 7807 ProblemDetail (Problem Details for HTTP APIs)–based error handling.
19+
**Modern, type-safe OpenAPI client generation** — powered by **Spring Boot 3.4**, **Java 21**, and **OpenAPI Generator
20+
7.16.0**.
21+
This repository demonstrates a production-grade architecture where backend and client are fully aligned through
22+
generics, enabling nested generic envelopes (ServiceResponse<Page<T>>) and RFC 7807 ProblemDetail (Problem Details for
23+
HTTP APIs)–based error handling.
2124

2225
---
2326

@@ -36,14 +39,16 @@ This repository demonstrates a production-grade architecture where backend and c
3639
* 📘 [Adoption Guides](#-adoption-guides)
3740
* 🔗 [References & Links](#-references--links)
3841

39-
> *A clean architecture pattern for building generics-aware OpenAPI clients that stay fully type-safe, consistent, and boilerplate-free.*
42+
> *A clean architecture pattern for building generics-aware OpenAPI clients that stay fully type-safe, consistent, and
43+
boilerplate-free.*
4044

4145
---
4246

4347
## 📦 Modules
4448

4549
* [**customer-service**](customer-service/README.md) — sample backend exposing `/v3/api-docs.yaml` via Springdoc
46-
* [**customer-service-client**](customer-service-client/README.md) — generated OpenAPI client with generics-aware wrappers
50+
* [**customer-service-client**](customer-service-client/README.md) — generated OpenAPI client with generics-aware
51+
wrappers
4752

4853
---
4954

@@ -54,6 +59,7 @@ When backend APIs wrap payloads in `ServiceResponse<T>` (e.g., the unified `{ da
5459
the generator produces **duplicated models per endpoint** instead of a single reusable generic base.
5560

5661
This results in:
62+
5763
* ❌ Dozens of almost-identical response classes
5864
* ❌ Higher maintenance overhead
5965
* ❌ Harder to evolve a single envelope contract across services
@@ -66,7 +72,8 @@ This project provides a **full-stack pattern** to align Spring Boot services and
6672

6773
### Server-Side (Producer)
6874

69-
A `Springdoc` customizer automatically scans controller return types and marks generic wrappers (`ServiceResponse<T>`) using vendor extensions:
75+
A `Springdoc` customizer automatically scans controller return types and marks generic wrappers (`ServiceResponse<T>`)
76+
using vendor extensions:
7077

7178
```yaml
7279
x-api-wrapper: true
@@ -77,15 +84,18 @@ x-data-item: CustomerDto
7784
7885
### Client-Side (Consumer)
7986
80-
Mustache overlays redefine OpenAPI templates to generate **thin, type-safe wrappers** extending a reusable base class `ServiceClientResponse<T>`.
87+
Mustache overlays redefine OpenAPI templates to generate **thin, type-safe wrappers** extending a reusable base class
88+
`ServiceClientResponse<T>`.
8189

8290
**Example generated output:**
8391

8492
```java
85-
public class ServiceResponseCustomerDto extends ServiceClientResponse<CustomerDto> {}
93+
public class ServiceResponseCustomerDto extends ServiceClientResponse<CustomerDto> {
94+
}
8695
```
8796

88-
This pattern supports **nested generics** like `ServiceClientResponse<Page<CustomerDto>>` and maps all error responses into **ProblemDetail** objects.
97+
This pattern supports **nested generics** like `ServiceClientResponse<Page<CustomerDto>>` and maps all error responses
98+
into **ProblemDetail** objects.
8999

90100
---
91101

@@ -98,7 +108,7 @@ This pattern supports **nested generics** like `ServiceClientResponse<Page<Custo
98108
</p>
99109

100110
| Layer | Description |
101-
| --------------------- | ------------------------------------------------------------------------- |
111+
|-----------------------|---------------------------------------------------------------------------|
102112
| **Server (Producer)** | Publishes OpenAPI 3.1 spec with auto-registered wrapper schemas |
103113
| **Client (Consumer)** | Uses OpenAPI Generator 7.16.0 + Mustache overlays for generics support |
104114
| **Envelope Model** | Unified `{ data, meta }` response structure |
@@ -159,8 +169,16 @@ The unified envelope applies to both single and paged responses. Below is a page
159169
{
160170
"data": {
161171
"content": [
162-
{ "customerId": 1, "name": "Jane Doe", "email": "[email protected]" },
163-
{ "customerId": 2, "name": "John Smith", "email": "[email protected]" }
172+
{
173+
"customerId": 1,
174+
"name": "Jane Doe",
175+
"email": "[email protected]"
176+
},
177+
{
178+
"customerId": 2,
179+
"name": "John Smith",
180+
"email": "[email protected]"
181+
}
164182
],
165183
"page": 0,
166184
"size": 5,
@@ -172,7 +190,10 @@ The unified envelope applies to both single and paged responses. Below is a page
172190
"meta": {
173191
"serverTime": "2025-01-01T12:34:56Z",
174192
"sort": [
175-
{ "field": "CUSTOMER_ID", "direction": "ASC" }
193+
{
194+
"field": "CUSTOMER_ID",
195+
"direction": "ASC"
196+
}
176197
]
177198
}
178199
}
@@ -186,17 +207,20 @@ ServiceClientResponse<Page<CustomerDto>> resp =
186207
"Jane", null, 0, 5, SortField.CUSTOMER_ID, SortDirection.ASC);
187208
188209
Page<CustomerDto> page = resp.getData();
189-
for (CustomerDto c : page.content()) {
210+
for(
211+
CustomerDto c :page.
212+
213+
content()){
190214
// ...
191-
}
215+
}
192216
```
193217

194218
---
195219

196220
## 🧩 Tech Stack
197221

198222
| Component | Version | Purpose |
199-
| --------------------- | ------- | ------------------------------------- |
223+
|-----------------------|---------|---------------------------------------|
200224
| **Java** | 21 | Language baseline |
201225
| **Spring Boot** | 3.4.10 | REST + OpenAPI provider |
202226
| **Springdoc** | 2.8.13 | OpenAPI 3.1 integration |
@@ -244,8 +268,13 @@ See the detailed integration steps under [`docs/adoption`](docs/adoption):
244268
## 🔗 References & Links
245269

246270
* 🌐 [GitHub Pages — Adoption Guides](https://bsayli.github.io/spring-boot-openapi-generics-clients/)
247-
* 📘 [Medium — Type-Safe Generic API Responses](https://medium.com/@baris.sayli/type-safe-generic-api-responses-with-spring-boot-3-4-openapi-generator-and-custom-templates-ccd93405fb04)
248-
* 💬 [Dev.to — Type-Safe OpenAPI Clients Without Boilerplate](https://dev.to/barissayli/spring-boot-openapi-generator-type-safe-generic-api-clients-without-boilerplate-3a8f)
271+
*
272+
273+
📘 [Medium — Type-Safe Generic API Responses](https://medium.com/@baris.sayli/type-safe-generic-api-responses-with-spring-boot-3-4-openapi-generator-and-custom-templates-ccd93405fb04)
274+
275+
*
276+
277+
💬 [Dev.to — Type-Safe OpenAPI Clients Without Boilerplate](https://dev.to/barissayli/spring-boot-openapi-generator-type-safe-generic-api-clients-without-boilerplate-3a8f)
249278

250279
---
251280

customer-service-client/README.md

Lines changed: 70 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ mvn -q clean install
3636
## ✅ What You Get
3737

3838
* Java client using **OpenAPI Generator 7.16.0** with the **Spring `RestClient`** library.
39-
* A reusable generic base: `io.github.bsayli.openapi.client.common.ServiceClientResponse<T>` containing `data` and `meta`.
39+
* A reusable generic base: `io.github.bsayli.openapi.client.common.ServiceClientResponse<T>` containing `data` and
40+
`meta`.
4041
* **Nested generics support**: wrappers such as `ServiceClientResponse<Page<CustomerDto>>`.
4142
* **RFC 7807 Problem decoding** via `ClientProblemException`.
4243
* **Spring Boot configuration** for pooled HttpClient5 + `RestClientCustomizer` for error handling.
@@ -46,7 +47,8 @@ mvn -q clean install
4647

4748
## 🧠 How the Thin Wrappers Are Produced
4849

49-
Server marks wrapper schemas with vendor extensions. The Mustache overlay generates thin wrappers extending the generic base.
50+
Server marks wrapper schemas with vendor extensions. The Mustache overlay generates thin wrappers extending the generic
51+
base.
5052

5153
```mustache
5254
{{! Generics-aware thin wrapper }}
@@ -85,11 +87,13 @@ public class ServiceClientResponse<T> {
8587
private ClientMeta meta;
8688
}
8789

88-
public record ClientMeta(Instant serverTime, List<ClientSort> sort) {}
90+
public record ClientMeta(Instant serverTime, List<ClientSort> sort) {
91+
}
8992

9093
public record Page<T>(List<T> content, int page, int size,
9194
long totalElements, int totalPages,
92-
boolean hasNext, boolean hasPrev) {}
95+
boolean hasNext, boolean hasPrev) {
96+
}
9397
```
9498

9599
**Problem Exception:**
@@ -106,47 +110,49 @@ public class ClientProblemException extends RuntimeException {
106110
## ⚙️ Spring Configuration (Production-Ready)
107111

108112
```java
113+
109114
@Configuration
110115
public class CustomerApiClientConfig {
111116

112-
@Bean
113-
RestClientCustomizer problemDetailStatusHandler(ObjectMapper om) {
114-
return builder -> builder.defaultStatusHandler(
115-
HttpStatusCode::isError,
116-
(request, response) -> {
117-
ProblemDetail pd = null;
118-
try (var is = response.getBody()) {
119-
pd = om.readValue(is, ProblemDetail.class);
120-
} catch (Exception ignore) {}
121-
throw new ClientProblemException(pd, response.getStatusCode().value());
122-
});
123-
}
124-
125-
@Bean(destroyMethod = "close")
126-
CloseableHttpClient customerHttpClient(
127-
@Value("${customer.api.max-connections-total:64}") int maxTotal,
128-
@Value("${customer.api.max-connections-per-route:16}") int maxPerRoute) {
129-
var cm = PoolingHttpClientConnectionManagerBuilder.create()
130-
.setMaxConnTotal(maxTotal)
131-
.setMaxConnPerRoute(maxPerRoute)
132-
.build();
133-
return HttpClients.custom()
134-
.setConnectionManager(cm)
135-
.evictExpiredConnections()
136-
.evictIdleConnections(org.apache.hc.core5.util.TimeValue.ofSeconds(30))
137-
.setUserAgent("customer-service-client")
138-
.disableAutomaticRetries()
139-
.build();
140-
}
141-
142-
@Bean
143-
RestClient customerRestClient(RestClient.Builder builder,
144-
HttpComponentsClientHttpRequestFactory rf,
145-
List<RestClientCustomizer> customizers) {
146-
builder.requestFactory(rf);
147-
if (customizers != null) customizers.forEach(c -> c.customize(builder));
148-
return builder.build();
149-
}
117+
@Bean
118+
RestClientCustomizer problemDetailStatusHandler(ObjectMapper om) {
119+
return builder -> builder.defaultStatusHandler(
120+
HttpStatusCode::isError,
121+
(request, response) -> {
122+
ProblemDetail pd = null;
123+
try (var is = response.getBody()) {
124+
pd = om.readValue(is, ProblemDetail.class);
125+
} catch (Exception ignore) {
126+
}
127+
throw new ClientProblemException(pd, response.getStatusCode().value());
128+
});
129+
}
130+
131+
@Bean(destroyMethod = "close")
132+
CloseableHttpClient customerHttpClient(
133+
@Value("${customer.api.max-connections-total:64}") int maxTotal,
134+
@Value("${customer.api.max-connections-per-route:16}") int maxPerRoute) {
135+
var cm = PoolingHttpClientConnectionManagerBuilder.create()
136+
.setMaxConnTotal(maxTotal)
137+
.setMaxConnPerRoute(maxPerRoute)
138+
.build();
139+
return HttpClients.custom()
140+
.setConnectionManager(cm)
141+
.evictExpiredConnections()
142+
.evictIdleConnections(org.apache.hc.core5.util.TimeValue.ofSeconds(30))
143+
.setUserAgent("customer-service-client")
144+
.disableAutomaticRetries()
145+
.build();
146+
}
147+
148+
@Bean
149+
RestClient customerRestClient(RestClient.Builder builder,
150+
HttpComponentsClientHttpRequestFactory rf,
151+
List<RestClientCustomizer> customizers) {
152+
builder.requestFactory(rf);
153+
if (customizers != null) customizers.forEach(c -> c.customize(builder));
154+
return builder.build();
155+
}
150156
}
151157
```
152158

@@ -166,23 +172,24 @@ customer.api.read-timeout-seconds=15
166172
## 🧩 Adapter Pattern Example
167173

168174
```java
175+
169176
@Service
170177
public class CustomerClientAdapterImpl implements CustomerClientAdapter {
171-
private final CustomerControllerApi api;
172-
173-
public CustomerClientAdapterImpl(CustomerControllerApi api) {
174-
this.api = api;
175-
}
176-
177-
@Override
178-
public ServiceClientResponse<Page<CustomerDto>> getCustomers(
179-
String name, String email, Integer page, Integer size,
180-
SortField sortBy, SortDirection direction) {
181-
return api.getCustomers(
182-
name, email, page, size,
183-
sortBy != null ? sortBy.value() : SortField.CUSTOMER_ID.value(),
184-
direction != null ? direction.value() : SortDirection.ASC.value());
185-
}
178+
private final CustomerControllerApi api;
179+
180+
public CustomerClientAdapterImpl(CustomerControllerApi api) {
181+
this.api = api;
182+
}
183+
184+
@Override
185+
public ServiceClientResponse<Page<CustomerDto>> getCustomers(
186+
String name, String email, Integer page, Integer size,
187+
SortField sortBy, SortDirection direction) {
188+
return api.getCustomers(
189+
name, email, page, size,
190+
sortBy != null ? sortBy.value() : SortField.CUSTOMER_ID.value(),
191+
direction != null ? direction.value() : SortDirection.ASC.value());
192+
}
186193
}
187194
```
188195

@@ -205,11 +212,12 @@ var serverTime = resp.getMeta().serverTime();
205212
Error handling:
206213

207214
```java
208-
try {
209-
customerClientAdapter.getCustomer(999);
210-
} catch (ClientProblemException ex) {
211-
var pd = ex.getProblem();
212-
// pd.getTitle(), pd.getDetail(), pd.getErrorCode(), etc.
215+
try{
216+
customerClientAdapter.getCustomer(999);
217+
}catch(
218+
ClientProblemException ex){
219+
var pd = ex.getProblem();
220+
// pd.getTitle(), pd.getDetail(), pd.getErrorCode(), etc.
213221
}
214222
```
215223

customer-service-client/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
<groupId>io.github.bsayli</groupId>
88
<artifactId>customer-service-client</artifactId>
9-
<version>0.6.8</version>
9+
<version>0.7.0</version>
1010
<name>customer-service-client</name>
1111
<description>Generated client (RestClient) using generics-aware OpenAPI templates</description>
1212
<packaging>jar</packaging>

customer-service-client/src/main/resources/customer-api-docs.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ openapi: 3.1.0
22
info:
33
title: Customer Service API
44
description: Customer Service API with type-safe generic responses using OpenAPI
5-
version: 0.6.8
5+
version: 0.7.0
66
servers:
77
- url: http://localhost:8084/customer-service
88
description: Local service URL

0 commit comments

Comments
 (0)