Skip to content

Commit 6a8ba94

Browse files
committed
chore(release): bump to v0.6.2 with improved API semantics, error handling, and client docs
### Core changes - Upgraded **Spring Boot** from 3.4.9 → 3.4.10 (latest patch, bugfixes, stability). - Standardized package root to `io.github.bsayli.customerservice` (cleaner structure, consistent with client package). - `CustomerController#createCustomer` now returns **201 Created** with `Location` header (REST-compliant resource creation). - Added `CustomerControllerAdvice` with detailed error responses: * validation errors mapped to field-level violations * type mismatch & unreadable requests logged clearly * unified `ServiceResponse<ErrorPayload>` contract for clients - Updated `application.yaml` with explicit error inclusion settings (messages + violations, no stacktrace leakage). ### Documentation & tests - Polished all **README.md** files for clarity: * simpler TL;DR sections * better examples for Spring wiring & adapter pattern * consistent terminology (removed “demo” wording) - Expanded integration tests: full CRUD coverage + validation & error cases. - Enhanced generated client docs: show strong typing, thin wrappers, adapter isolation. ### CI/CD - GitHub Actions workflow updated: * uploads OpenAPI spec (YAML) as artifact * uploads generated client sources * ensures JDK 21 setup across all jobs --- 💡 **Why it matters:** For client developers, v0.6.2 delivers a more predictable API contract (201 + Location, unified error payloads) and safer integration (no leaking stacktraces, consistent generics). Docs and CI improvements make it easier to adopt and trust the OpenAPI generics pattern in real projects.
1 parent e7c99ce commit 6a8ba94

36 files changed

+384
-163
lines changed

.github/workflows/build.yml

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ on:
55
branches: [ main ]
66
pull_request:
77

8+
permissions:
9+
contents: read
10+
811
jobs:
912
build:
1013
runs-on: ubuntu-latest
@@ -24,4 +27,25 @@ jobs:
2427

2528
- name: Build customer-service-client
2629
run: mvn -B clean verify
27-
working-directory: customer-service-client
30+
working-directory: customer-service-client
31+
32+
- name: List generated sources and spec
33+
run: |
34+
ls -R customer-service-client/target/generated-sources/openapi/src/gen/java || true
35+
ls -l customer-service-client/target/classes/customer-api-docs.yaml || true
36+
37+
- name: Upload generated client sources
38+
uses: actions/upload-artifact@v4
39+
with:
40+
name: generated-client-sources
41+
path: customer-service-client/target/generated-sources/openapi/src/gen/java
42+
if-no-files-found: warn
43+
retention-days: 7
44+
45+
- name: Upload OpenAPI spec (YAML)
46+
uses: actions/upload-artifact@v4
47+
with:
48+
name: customer-api-docs
49+
path: customer-service-client/target/classes/customer-api-docs.yaml
50+
if-no-files-found: warn
51+
retention-days: 7

README.md

Lines changed: 75 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# spring-boot-openapi-generics-clients
22

33
[![Build](https://github.com/bsayli/spring-boot-openapi-generics-clients/actions/workflows/build.yml/badge.svg)](https://github.com/bsayli/spring-boot-openapi-generics-clients/actions/workflows/build.yml)
4-
[![Release](https://img.shields.io/github/v/release/bsayli/spring-boot-openapi-generics-clients?logo=github&label=release)](https://github.com/bsayli/spring-boot-openapi-generics-clients/releases/latest)
4+
[![Release](https://img.shields.io/github/v/release/bsayli/spring-boot-openapi-generics-clients?logo=github\&label=release)](https://github.com/bsayli/spring-boot-openapi-generics-clients/releases/latest)
55
[![Java](https://img.shields.io/badge/Java-21-red?logo=openjdk)](https://openjdk.org/projects/jdk/21/)
66
[![Spring Boot](https://img.shields.io/badge/Spring%20Boot-3.4.9-green?logo=springboot)](https://spring.io/projects/spring-boot)
77
[![OpenAPI Generator](https://img.shields.io/badge/OpenAPI%20Generator-7.x-blue?logo=openapiinitiative)](https://openapi-generator.tech/)
@@ -14,7 +14,7 @@
1414
</p>
1515

1616
**Type-safe client generation with Spring Boot & OpenAPI using generics.**
17-
This repository demonstrates how to teach OpenAPI Generator to work with generics in order to avoid boilerplate, reduce
17+
This repository demonstrates how to extend OpenAPI Generator to work with generics in order to avoid boilerplate, reduce
1818
duplicated wrappers, and keep client code clean.
1919

2020
---
@@ -43,9 +43,40 @@ This project shows how to:
4343

4444
---
4545

46+
### How it works (under the hood)
47+
48+
At generation time, the reference service **auto-registers** wrapper schemas in the OpenAPI doc:
49+
50+
* A Spring `OpenApiCustomizer` scans controller return types and unwraps `ResponseEntity`, `CompletionStage`, Reactor (
51+
`Mono`/`Flux`), etc. until it reaches `ServiceResponse<T>`.
52+
* For every discovered `T`, it adds a `ServiceResponse{T}` schema that composes the base envelope + the concrete `data`
53+
type, and marks it with vendor extensions:
54+
55+
* `x-api-wrapper: true`
56+
* `x-api-wrapper-datatype: <T>`
57+
58+
The Java client then uses a tiny Mustache override to render **thin shells** for those marked schemas:
59+
60+
```mustache
61+
// api_wrapper.mustache
62+
public class {{classname}}
63+
extends io.github.bsayli.openapi.client.common.ServiceClientResponse<{{vendorExtensions.x-api-wrapper-datatype}}> {
64+
}
65+
```
66+
67+
This is what turns e.g. `ServiceResponseCustomerCreateResponse` into:
68+
69+
```java
70+
public class ServiceResponseCustomerCreateResponse
71+
extends ServiceClientResponse<CustomerCreateResponse> {
72+
}
73+
```
74+
75+
---
76+
4677
## ⚡ Quick Start
4778

48-
Run the sample service:
79+
Run the reference service:
4980

5081
```bash
5182
cd customer-service
@@ -66,11 +97,11 @@ ServiceClientResponse<CustomerCreateResponse> response =
6697
customerControllerApi.createCustomer(request);
6798
```
6899

69-
### 🖼 Demo Swagger Screenshot
100+
### 🖼 Swagger Screenshot
70101

71102
Here’s what the `create customer` endpoint looks like in Swagger UI after running the service:
72103

73-
![Customer create demo](docs/images/swagger-customer-create.png)
104+
![Customer create example](docs/images/swagger-customer-create.png)
74105

75106
### 🖼 Generated Client Wrapper
76107

@@ -83,6 +114,18 @@ Comparison of how OpenAPI Generator outputs looked **before** vs **after** addin
83114
**After (thin generic wrapper):**
84115

85116
![Generated client (after)](docs/images/generated-client-wrapper-after.png)
117+
118+
---
119+
120+
## ✅ Verify in 60 Seconds
121+
122+
1. Clone this repo
123+
2. Run `mvn clean install -q`
124+
3. Open `customer-service-client/target/generated-sources/...`
125+
4. See the generated wrappers → they now extend a **generic base class** instead of duplicating fields.
126+
127+
You don’t need to write a single line of code — the generator does the work.
128+
86129
---
87130

88131
## 🛠 Tech Stack & Features
@@ -120,23 +163,24 @@ spring-boot-openapi-generics-clients/
120163

121164
### ✨ Usage Example: Adapter Interface
122165

123-
Sometimes you don’t want to expose all the thin wrappers directly.
166+
Sometimes you don’t want to expose all the thin wrappers directly.
124167
A simple adapter interface can consolidate them into clean, type-safe methods:
125168

126169
```java
127170
public interface CustomerClientAdapter {
128-
ServiceClientResponse<CustomerCreateResponse> createCustomer(CustomerCreateRequest request);
171+
ServiceClientResponse<CustomerCreateResponse> createCustomer(CustomerCreateRequest request);
129172

130-
ServiceClientResponse<CustomerDto> getCustomer(Integer customerId);
173+
ServiceClientResponse<CustomerDto> getCustomer(Integer customerId);
131174

132-
ServiceClientResponse<CustomerListResponse> getCustomers();
175+
ServiceClientResponse<CustomerListResponse> getCustomers();
133176

134-
ServiceClientResponse<CustomerUpdateResponse> updateCustomer(
135-
Integer customerId, CustomerUpdateRequest request);
177+
ServiceClientResponse<CustomerUpdateResponse> updateCustomer(
178+
Integer customerId, CustomerUpdateRequest request);
136179

137-
ServiceClientResponse<CustomerDeleteResponse> deleteCustomer(Integer customerId);
180+
ServiceClientResponse<CustomerDeleteResponse> deleteCustomer(Integer customerId);
138181
}
139182
```
183+
140184
---
141185

142186
## 🔍 Why This Matters
@@ -163,7 +207,7 @@ This pattern is useful when:
163207

164208
## 🔧 How to Run
165209

166-
1. **Start the sample service**
210+
1. **Start the reference service**
167211

168212
```bash
169213
cd customer-service
@@ -186,6 +230,24 @@ This pattern is useful when:
186230

187231
---
188232

233+
## 👤 Who Should Use This?
234+
235+
* Backend developers maintaining multiple microservices
236+
* API platform teams standardizing response envelopes
237+
* Teams already invested in OpenAPI Generator looking to reduce boilerplate
238+
239+
---
240+
241+
## ⚠️ Why Not Use It?
242+
243+
This project may not be the right fit if:
244+
245+
* Your APIs do **not** use a common response wrapper
246+
* You are fine with duplicated wrapper models
247+
* You don’t generate client code from OpenAPI specs
248+
249+
---
250+
189251
## 📖 Related Article
190252

191253
This repository is based on my article:

customer-service-client/README.md

Lines changed: 61 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,32 @@
11
# customer-service-client
22

3-
Generated Java client for the demo **customer-service**, showcasing **type-safe generic responses** with OpenAPI + a
3+
Generated Java client for the **customer-service**, showcasing **type-safe generic responses** with OpenAPI + a
44
custom Mustache template (wrapping payloads in a reusable `ServiceClientResponse<T>`).
55

66
This module demonstrates how to evolve OpenAPI Generator with minimal customization to support generic response
77
envelopes — avoiding duplicated wrappers and preserving strong typing.
88

99
---
1010

11+
## 🔧 TL;DR: Generate in 1 minute
12+
13+
```bash
14+
# 1) Start the customer service server (in another shell)
15+
cd customer-service && mvn -q spring-boot:run
16+
17+
# 2) Pull the OpenAPI spec into the client module
18+
cd ../customer-service-client
19+
curl -s http://localhost:8084/customer-service/v3/api-docs.yaml \
20+
-o src/main/resources/customer-api-docs.yaml
21+
22+
# 3) Generate & compile the client
23+
mvn -q clean install
24+
```
25+
26+
Generated sources → `target/generated-sources/openapi/src/gen/java`
27+
28+
---
29+
1130
## ✅ What You Get
1231

1332
* Generated code using **OpenAPI Generator** (`restclient` with Spring Framework `RestClient`).
@@ -42,11 +61,14 @@ curl -s http://localhost:8084/customer-service/v3/api-docs.yaml \
4261
mvn clean install
4362
```
4463

45-
Generated sources will be placed under:
64+
### What got generated?
4665

47-
```
48-
target/generated-sources/openapi/src/gen/java
49-
```
66+
Look for these classes under `target/generated-sources/openapi/src/gen/java`:
67+
68+
* `io.github.bsayli.openapi.client.generated.dto.ServiceResponseCustomerCreateResponse`
69+
* `...ServiceResponseCustomerUpdateResponse`, etc.
70+
71+
Each is a **thin shell** extending `ServiceClientResponse<PayloadType>`.
5072

5173
---
5274

@@ -108,6 +130,10 @@ public void createCustomer() {
108130
}
109131
```
110132

133+
> Tip — The return type is strongly typed: `ServiceClientResponse<CustomerCreateResponse>`.
134+
> You can safely navigate `resp.getData().getCustomer()` without casting.
135+
> Handle non-2xx via Spring exceptions (e.g., `HttpClientErrorException`) as usual.
136+
111137
---
112138

113139
### Option A.2 — Alternative with HttpClient5 (connection pooling)
@@ -161,7 +187,7 @@ public class CustomerApiClientConfig {
161187
@Bean
162188
ApiClient customerApiClient(RestClient customerRestClient,
163189
@Value("${customer.api.base-url}") String baseUrl) {
164-
return new ApiClient(restClient).setBasePath(baseUrl);
190+
return new ApiClient(customerRestClient).setBasePath(baseUrl);
165191
}
166192

167193
@Bean
@@ -257,6 +283,20 @@ public class ServiceResponseCustomerCreateResponse
257283

258284
Only this Mustache partial is customized. All other models use stock templates.
259285

286+
### Template overlay (Mustache)
287+
288+
This module overlays **two** tiny Mustache files on top of the stock Java generator:
289+
290+
* `src/main/resources/openapi-templates/api_wrapper.mustache`
291+
* `src/main/resources/openapi-templates/model.mustache`
292+
293+
At build time, the Maven `maven-dependency-plugin` unpacks the upstream templates and the
294+
`maven-resources-plugin` overlays the two local files. That’s what enables thin generic wrappers.
295+
296+
**Disable templates (optional):**
297+
set `<templateDirectory>` to a non-existent path or comment the overlay steps in `pom.xml`
298+
to compare stock output vs generic wrappers.
299+
260300
---
261301

262302
## 🧪 Tests
@@ -277,14 +317,25 @@ It enqueues responses for **all CRUD operations** and asserts correct mapping in
277317
* Dependencies like `spring-web`, `spring-context`, `jackson-*`, `jakarta.*` are marked **provided**; your host app
278318
supplies them.
279319
* Generator options: Spring 6 `RestClient`, Jakarta EE, Jackson, Java 21.
280-
* OpenAPI spec path:
320+
* OpenAPI spec lives at: `src/main/resources/customer-api-docs.yaml`.
321+
If you re-run the server and want an updated client, re-pull the spec:
281322

282-
```
283-
src/main/resources/customer-api-docs.yaml
284-
```
323+
```bash
324+
curl -s http://localhost:8084/customer-service/v3/api-docs.yaml \
325+
-o src/main/resources/customer-api-docs.yaml
326+
mvn -q clean install
327+
```
285328

286329
---
287330

288331
## 🛡 License
289332

290333
This repository is licensed under **MIT** (root `LICENSE`). Submodules inherit the license.
334+
335+
### Packaging note (optional)
336+
337+
This module is **reference-oriented**. If you want to publish it as a reusable library later:
338+
339+
* remove `provided` scopes and pin minimal runtime deps,
340+
* add a semantic version and release process (e.g., GitHub Release + `mvn deploy` to Maven Central),
341+
* keep the Mustache overlay in-repo for transparent builds.

customer-service-client/pom.xml

Lines changed: 2 additions & 2 deletions
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.1</version>
9+
<version>0.6.2</version>
1010
<name>customer-service-client</name>
1111
<description>Generated client (RestClient) using generics-aware OpenAPI templates</description>
1212
<packaging>jar</packaging>
@@ -16,7 +16,7 @@
1616
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
1717
<java.version>21</java.version>
1818

19-
<spring-boot.version>3.4.9</spring-boot.version>
19+
<spring-boot.version>3.4.10</spring-boot.version>
2020
<openapi.generator.version>7.15.0</openapi.generator.version>
2121
<maven.compiler.plugin.version>3.13.0</maven.compiler.plugin.version>
2222
<build.helper.plugin.version>3.6.0</build.helper.plugin.version>

0 commit comments

Comments
 (0)