claudient 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,333 @@
1
+ > 🇳🇱 Nederlandse versie. [Engelse versie](../spring-boot.md).
2
+
3
+ # Spring Boot Skill
4
+
5
+ ## Wanneer te activeren
6
+ - Een Spring Boot REST API of microservice bouwen
7
+ - Spring Data JPA instellen met Hibernate en PostgreSQL/MySQL
8
+ - Spring Security configureren (JWT, OAuth2, sessie-gebaseerde authenticatie)
9
+ - Spring Boot tests schrijven (unit-tests met Mockito, integratietests met TestRestTemplate of MockMvc)
10
+ - Spring Cloud instellen (service discovery, config server, API gateway)
11
+ - Asynchrone verwerking implementeren met `@Async` of Spring Events
12
+ - Profielen, properties en externe configuratie instellen
13
+ - Aangepaste Spring Boot starters of auto-configuraties schrijven
14
+
15
+ ## Wanneer NIET te gebruiken
16
+ - Quarkus- of Micronaut-projecten — andere DI- en configuratiemodellen
17
+ - Gewoon Java zonder Spring — de overhead is niet gerechtvaardigd voor eenvoudige scripts
18
+ - Android-projecten — volledig ander ecosysteem
19
+ - Jakarta EE/WildFly — ander applicatieservermodel
20
+
21
+ ## Instructies
22
+
23
+ ### Projectstructuur
24
+ ```
25
+ src/
26
+ ├── main/
27
+ │ ├── java/com/example/app/
28
+ │ │ ├── AppApplication.java # @SpringBootApplication entry point
29
+ │ │ ├── config/ # @Configuration beans
30
+ │ │ ├── controller/ # @RestController — alleen HTTP-laag
31
+ │ │ ├── service/ # Bedrijfslogica — @Service
32
+ │ │ ├── repository/ # @Repository — gegevenstoegang
33
+ │ │ ├── domain/ # @Entity modellen
34
+ │ │ ├── dto/ # Request/response-vormen (records)
35
+ │ │ ├── exception/ # @ControllerAdvice foutafhandeling
36
+ │ │ └── security/ # Security-configuratie en filters
37
+ │ └── resources/
38
+ │ ├── application.yml # Basisconfiguratie
39
+ │ ├── application-dev.yml # Ontwikkelomgeving-overschrijvingen
40
+ │ └── application-prod.yml # Productieomgeving-overschrijvingen
41
+ └── test/
42
+ └── java/com/example/app/
43
+ ├── controller/ # MockMvc / @WebMvcTest
44
+ ├── service/ # Unit-tests met Mockito
45
+ └── integration/ # @SpringBootTest volledige context
46
+ ```
47
+
48
+ ### Applicatie-ingangspunt
49
+ ```java
50
+ @SpringBootApplication
51
+ public class AppApplication {
52
+ public static void main(String[] args) {
53
+ SpringApplication.run(AppApplication.class, args);
54
+ }
55
+ }
56
+ ```
57
+
58
+ ### application.yml structuur
59
+ ```yaml
60
+ spring:
61
+ application:
62
+ name: my-service
63
+ datasource:
64
+ url: ${DATABASE_URL}
65
+ username: ${DB_USER}
66
+ password: ${DB_PASSWORD}
67
+ jpa:
68
+ hibernate:
69
+ ddl-auto: validate # Nooit 'create' of 'update' in productie
70
+ show-sql: false
71
+ properties:
72
+ hibernate:
73
+ format_sql: true
74
+
75
+ server:
76
+ port: ${PORT:8080}
77
+
78
+ # Aangepaste properties — gebruik altijd @ConfigurationProperties, nooit @Value voor groepen
79
+ app:
80
+ jwt:
81
+ secret: ${JWT_SECRET}
82
+ expiration-ms: 86400000
83
+ ```
84
+
85
+ ### Entity en Repository
86
+ ```java
87
+ // domain/User.java
88
+ @Entity
89
+ @Table(name = "users")
90
+ public class User {
91
+ @Id
92
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
93
+ private Long id;
94
+
95
+ @Column(nullable = false, unique = true, length = 320)
96
+ private String email;
97
+
98
+ @Column(nullable = false)
99
+ private String passwordHash;
100
+
101
+ @CreationTimestamp
102
+ @Column(updatable = false)
103
+ private Instant createdAt;
104
+
105
+ // No-arg constructor required by JPA
106
+ protected User() {}
107
+
108
+ public User(String email, String passwordHash) {
109
+ this.email = email;
110
+ this.passwordHash = passwordHash;
111
+ }
112
+ // getters only — no setters on entities
113
+ }
114
+
115
+ // repository/UserRepository.java
116
+ public interface UserRepository extends JpaRepository<User, Long> {
117
+ Optional<User> findByEmail(String email);
118
+ boolean existsByEmail(String email);
119
+
120
+ // JPQL for complex queries
121
+ @Query("SELECT u FROM User u WHERE u.createdAt > :since")
122
+ List<User> findRecentUsers(@Param("since") Instant since);
123
+ }
124
+ ```
125
+
126
+ ### Service-laag
127
+ ```java
128
+ @Service
129
+ @Transactional(readOnly = true) // Standaard alleen-lezen; overschrijf voor schrijfoperaties
130
+ public class UserService {
131
+ private final UserRepository userRepo;
132
+ private final PasswordEncoder passwordEncoder;
133
+
134
+ public UserService(UserRepository userRepo, PasswordEncoder passwordEncoder) {
135
+ this.userRepo = userRepo;
136
+ this.passwordEncoder = passwordEncoder;
137
+ }
138
+
139
+ @Transactional // Overschrijving: deze methode schrijft
140
+ public UserDto createUser(CreateUserRequest request) {
141
+ if (userRepo.existsByEmail(request.email())) {
142
+ throw new ConflictException("Email already in use");
143
+ }
144
+ User user = new User(request.email(), passwordEncoder.encode(request.password()));
145
+ return UserDto.from(userRepo.save(user));
146
+ }
147
+
148
+ public UserDto getById(Long id) {
149
+ return userRepo.findById(id)
150
+ .map(UserDto::from)
151
+ .orElseThrow(() -> new NotFoundException("User " + id + " not found"));
152
+ }
153
+ }
154
+ ```
155
+
156
+ ### Controller-laag
157
+ ```java
158
+ @RestController
159
+ @RequestMapping("/api/v1/users")
160
+ @Validated
161
+ public class UserController {
162
+ private final UserService userService;
163
+
164
+ public UserController(UserService userService) {
165
+ this.userService = userService;
166
+ }
167
+
168
+ @PostMapping
169
+ @ResponseStatus(HttpStatus.CREATED)
170
+ public UserDto createUser(@RequestBody @Valid CreateUserRequest request) {
171
+ return userService.createUser(request);
172
+ }
173
+
174
+ @GetMapping("/{id}")
175
+ public UserDto getUser(@PathVariable Long id) {
176
+ return userService.getById(id);
177
+ }
178
+
179
+ @GetMapping
180
+ public Page<UserDto> listUsers(Pageable pageable) {
181
+ return userService.findAll(pageable);
182
+ }
183
+ }
184
+ ```
185
+
186
+ ### DTO's met records
187
+ ```java
188
+ // dto/CreateUserRequest.java
189
+ public record CreateUserRequest(
190
+ @NotBlank @Email String email,
191
+ @NotBlank @Size(min = 8) String password
192
+ ) {}
193
+
194
+ // dto/UserDto.java
195
+ public record UserDto(Long id, String email, Instant createdAt) {
196
+ public static UserDto from(User user) {
197
+ return new UserDto(user.getId(), user.getEmail(), user.getCreatedAt());
198
+ }
199
+ }
200
+ ```
201
+
202
+ ### Globale uitzonderingsafhandeling
203
+ ```java
204
+ @RestControllerAdvice
205
+ public class GlobalExceptionHandler {
206
+
207
+ @ExceptionHandler(NotFoundException.class)
208
+ @ResponseStatus(HttpStatus.NOT_FOUND)
209
+ public ProblemDetail handleNotFound(NotFoundException ex) {
210
+ return ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
211
+ }
212
+
213
+ @ExceptionHandler(ConflictException.class)
214
+ @ResponseStatus(HttpStatus.CONFLICT)
215
+ public ProblemDetail handleConflict(ConflictException ex) {
216
+ return ProblemDetail.forStatusAndDetail(HttpStatus.CONFLICT, ex.getMessage());
217
+ }
218
+
219
+ @ExceptionHandler(MethodArgumentNotValidException.class)
220
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
221
+ public ProblemDetail handleValidation(MethodArgumentNotValidException ex) {
222
+ var detail = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, "Validation failed");
223
+ detail.setProperty("errors", ex.getBindingResult().getFieldErrors().stream()
224
+ .map(e -> e.getField() + ": " + e.getDefaultMessage())
225
+ .toList());
226
+ return detail;
227
+ }
228
+ }
229
+ ```
230
+
231
+ ### Spring Security — JWT
232
+ ```java
233
+ @Configuration
234
+ @EnableWebSecurity
235
+ public class SecurityConfig {
236
+
237
+ @Bean
238
+ public SecurityFilterChain filterChain(HttpSecurity http, JwtAuthFilter jwtFilter) throws Exception {
239
+ return http
240
+ .csrf(AbstractHttpConfigurer::disable)
241
+ .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
242
+ .authorizeHttpRequests(auth -> auth
243
+ .requestMatchers("/api/v1/auth/**", "/actuator/health").permitAll()
244
+ .anyRequest().authenticated()
245
+ )
246
+ .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
247
+ .build();
248
+ }
249
+
250
+ @Bean
251
+ public PasswordEncoder passwordEncoder() {
252
+ return new BCryptPasswordEncoder();
253
+ }
254
+ }
255
+ ```
256
+
257
+ ### Testen
258
+ ```java
259
+ // @WebMvcTest — controller slice test (geen volledige context)
260
+ @WebMvcTest(UserController.class)
261
+ class UserControllerTest {
262
+ @Autowired MockMvc mockMvc;
263
+ @MockitoBean UserService userService; // Spring Boot 3.4+: @MockitoBean
264
+
265
+ @Test
266
+ void createUser_returnsCreated() throws Exception {
267
+ given(userService.createUser(any())).willReturn(new UserDto(1L, "a@b.com", Instant.now()));
268
+
269
+ mockMvc.perform(post("/api/v1/users")
270
+ .contentType(MediaType.APPLICATION_JSON)
271
+ .content("""{"email":"a@b.com","password":"password123"}"""))
272
+ .andExpect(status().isCreated())
273
+ .andExpect(jsonPath("$.email").value("a@b.com"));
274
+ }
275
+ }
276
+
277
+ // @SpringBootTest — volledige integratietest
278
+ @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
279
+ @Transactional
280
+ class UserServiceIntegrationTest {
281
+ @Autowired UserService userService;
282
+
283
+ @Test
284
+ void createUser_persistsToDatabase() {
285
+ var user = userService.createUser(new CreateUserRequest("a@b.com", "password123"));
286
+ assertThat(user.email()).isEqualTo("a@b.com");
287
+ assertThat(user.id()).isNotNull();
288
+ }
289
+ }
290
+ ```
291
+
292
+ ### Spring Cloud patronen
293
+ ```yaml
294
+ # Service discovery met Eureka
295
+ spring:
296
+ application:
297
+ name: user-service
298
+ eureka:
299
+ client:
300
+ service-url:
301
+ defaultZone: http://eureka-server:8761/eureka/
302
+
303
+ # Config server client
304
+ spring:
305
+ config:
306
+ import: configserver:http://config-server:8888
307
+ ```
308
+
309
+ ```java
310
+ // Feign client voor aanroepen tussen services
311
+ @FeignClient(name = "order-service")
312
+ public interface OrderClient {
313
+ @GetMapping("/api/v1/orders/user/{userId}")
314
+ List<OrderDto> getOrdersByUser(@PathVariable Long userId);
315
+ }
316
+ ```
317
+
318
+ ## Voorbeeld
319
+
320
+ **Gebruiker:** Bouw een Spring Boot REST API voor een productcatalogus met CRUD-endpoints, PostgreSQL via JPA, bean-validatie, globale foutafhandeling en een `@WebMvcTest` voor het GET-endpoint.
321
+
322
+ **Verwachte uitvoer:**
323
+ - `Product` entity — `id`, `name`, `price` (BigDecimal), `stock`, `createdAt`
324
+ - `ProductDto` record + `CreateProductRequest` record met `@NotBlank` / `@Positive` validatie
325
+ - `ProductRepository extends JpaRepository<Product, Long>`
326
+ - `ProductService` — `@Transactional(readOnly = true)` op klasseniveau, `@Transactional` op schrijfoperaties
327
+ - `ProductController` — CRUD op `/api/v1/products`, `Pageable` op GET-lijst
328
+ - `GlobalExceptionHandler` — `NotFoundException` → 404, `MethodArgumentNotValidException` → 400 met veldfouten
329
+ - `ProductControllerTest` met `@WebMvcTest` + `@MockitoBean ProductService`
330
+
331
+ ---
332
+
333
+ > **Werk met ons:** Claudient wordt ondersteund door [Uitbreiden](https://uitbreiden.com/) — wij bouwen AI-producten en B2B-oplossingen met ontwikkelaarsgemeenschappen. [uitbreiden.com](https://uitbreiden.com/)
@@ -0,0 +1,331 @@
1
+ # Spring Boot Skill
2
+
3
+ ## When to activate
4
+ - Building a Spring Boot REST API or microservice
5
+ - Setting up Spring Data JPA with Hibernate and PostgreSQL/MySQL
6
+ - Configuring Spring Security (JWT, OAuth2, session-based auth)
7
+ - Writing Spring Boot tests (unit with Mockito, integration with TestRestTemplate or MockMvc)
8
+ - Setting up Spring Cloud (service discovery, config server, API gateway)
9
+ - Implementing async processing with `@Async` or Spring Events
10
+ - Configuring profiles, properties, and externalized configuration
11
+ - Writing custom Spring Boot starters or auto-configurations
12
+
13
+ ## When NOT to use
14
+ - Quarkus or Micronaut projects — different DI and config models
15
+ - Plain Java without Spring — overhead isn't justified for simple scripts
16
+ - Android projects — different ecosystem entirely
17
+ - Jakarta EE/WildFly — different application server model
18
+
19
+ ## Instructions
20
+
21
+ ### Project structure
22
+ ```
23
+ src/
24
+ ├── main/
25
+ │ ├── java/com/example/app/
26
+ │ │ ├── AppApplication.java # @SpringBootApplication entry point
27
+ │ │ ├── config/ # @Configuration beans
28
+ │ │ ├── controller/ # @RestController — HTTP layer only
29
+ │ │ ├── service/ # Business logic — @Service
30
+ │ │ ├── repository/ # @Repository — data access
31
+ │ │ ├── domain/ # @Entity models
32
+ │ │ ├── dto/ # Request/response shapes (records)
33
+ │ │ ├── exception/ # @ControllerAdvice error handling
34
+ │ │ └── security/ # Security config and filters
35
+ │ └── resources/
36
+ │ ├── application.yml # Base config
37
+ │ ├── application-dev.yml # Dev overrides
38
+ │ └── application-prod.yml # Prod overrides
39
+ └── test/
40
+ └── java/com/example/app/
41
+ ├── controller/ # MockMvc / @WebMvcTest
42
+ ├── service/ # Unit tests with Mockito
43
+ └── integration/ # @SpringBootTest full context
44
+ ```
45
+
46
+ ### Application entry point
47
+ ```java
48
+ @SpringBootApplication
49
+ public class AppApplication {
50
+ public static void main(String[] args) {
51
+ SpringApplication.run(AppApplication.class, args);
52
+ }
53
+ }
54
+ ```
55
+
56
+ ### application.yml structure
57
+ ```yaml
58
+ spring:
59
+ application:
60
+ name: my-service
61
+ datasource:
62
+ url: ${DATABASE_URL}
63
+ username: ${DB_USER}
64
+ password: ${DB_PASSWORD}
65
+ jpa:
66
+ hibernate:
67
+ ddl-auto: validate # Never 'create' or 'update' in production
68
+ show-sql: false
69
+ properties:
70
+ hibernate:
71
+ format_sql: true
72
+
73
+ server:
74
+ port: ${PORT:8080}
75
+
76
+ # Custom properties — always use @ConfigurationProperties, never @Value for groups
77
+ app:
78
+ jwt:
79
+ secret: ${JWT_SECRET}
80
+ expiration-ms: 86400000
81
+ ```
82
+
83
+ ### Entity and Repository
84
+ ```java
85
+ // domain/User.java
86
+ @Entity
87
+ @Table(name = "users")
88
+ public class User {
89
+ @Id
90
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
91
+ private Long id;
92
+
93
+ @Column(nullable = false, unique = true, length = 320)
94
+ private String email;
95
+
96
+ @Column(nullable = false)
97
+ private String passwordHash;
98
+
99
+ @CreationTimestamp
100
+ @Column(updatable = false)
101
+ private Instant createdAt;
102
+
103
+ // No-arg constructor required by JPA
104
+ protected User() {}
105
+
106
+ public User(String email, String passwordHash) {
107
+ this.email = email;
108
+ this.passwordHash = passwordHash;
109
+ }
110
+ // getters only — no setters on entities
111
+ }
112
+
113
+ // repository/UserRepository.java
114
+ public interface UserRepository extends JpaRepository<User, Long> {
115
+ Optional<User> findByEmail(String email);
116
+ boolean existsByEmail(String email);
117
+
118
+ // JPQL for complex queries
119
+ @Query("SELECT u FROM User u WHERE u.createdAt > :since")
120
+ List<User> findRecentUsers(@Param("since") Instant since);
121
+ }
122
+ ```
123
+
124
+ ### Service layer
125
+ ```java
126
+ @Service
127
+ @Transactional(readOnly = true) // Default read-only; override for writes
128
+ public class UserService {
129
+ private final UserRepository userRepo;
130
+ private final PasswordEncoder passwordEncoder;
131
+
132
+ public UserService(UserRepository userRepo, PasswordEncoder passwordEncoder) {
133
+ this.userRepo = userRepo;
134
+ this.passwordEncoder = passwordEncoder;
135
+ }
136
+
137
+ @Transactional // Override: this method writes
138
+ public UserDto createUser(CreateUserRequest request) {
139
+ if (userRepo.existsByEmail(request.email())) {
140
+ throw new ConflictException("Email already in use");
141
+ }
142
+ User user = new User(request.email(), passwordEncoder.encode(request.password()));
143
+ return UserDto.from(userRepo.save(user));
144
+ }
145
+
146
+ public UserDto getById(Long id) {
147
+ return userRepo.findById(id)
148
+ .map(UserDto::from)
149
+ .orElseThrow(() -> new NotFoundException("User " + id + " not found"));
150
+ }
151
+ }
152
+ ```
153
+
154
+ ### Controller layer
155
+ ```java
156
+ @RestController
157
+ @RequestMapping("/api/v1/users")
158
+ @Validated
159
+ public class UserController {
160
+ private final UserService userService;
161
+
162
+ public UserController(UserService userService) {
163
+ this.userService = userService;
164
+ }
165
+
166
+ @PostMapping
167
+ @ResponseStatus(HttpStatus.CREATED)
168
+ public UserDto createUser(@RequestBody @Valid CreateUserRequest request) {
169
+ return userService.createUser(request);
170
+ }
171
+
172
+ @GetMapping("/{id}")
173
+ public UserDto getUser(@PathVariable Long id) {
174
+ return userService.getById(id);
175
+ }
176
+
177
+ @GetMapping
178
+ public Page<UserDto> listUsers(Pageable pageable) {
179
+ return userService.findAll(pageable);
180
+ }
181
+ }
182
+ ```
183
+
184
+ ### DTOs with records
185
+ ```java
186
+ // dto/CreateUserRequest.java
187
+ public record CreateUserRequest(
188
+ @NotBlank @Email String email,
189
+ @NotBlank @Size(min = 8) String password
190
+ ) {}
191
+
192
+ // dto/UserDto.java
193
+ public record UserDto(Long id, String email, Instant createdAt) {
194
+ public static UserDto from(User user) {
195
+ return new UserDto(user.getId(), user.getEmail(), user.getCreatedAt());
196
+ }
197
+ }
198
+ ```
199
+
200
+ ### Global exception handler
201
+ ```java
202
+ @RestControllerAdvice
203
+ public class GlobalExceptionHandler {
204
+
205
+ @ExceptionHandler(NotFoundException.class)
206
+ @ResponseStatus(HttpStatus.NOT_FOUND)
207
+ public ProblemDetail handleNotFound(NotFoundException ex) {
208
+ return ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
209
+ }
210
+
211
+ @ExceptionHandler(ConflictException.class)
212
+ @ResponseStatus(HttpStatus.CONFLICT)
213
+ public ProblemDetail handleConflict(ConflictException ex) {
214
+ return ProblemDetail.forStatusAndDetail(HttpStatus.CONFLICT, ex.getMessage());
215
+ }
216
+
217
+ @ExceptionHandler(MethodArgumentNotValidException.class)
218
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
219
+ public ProblemDetail handleValidation(MethodArgumentNotValidException ex) {
220
+ var detail = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, "Validation failed");
221
+ detail.setProperty("errors", ex.getBindingResult().getFieldErrors().stream()
222
+ .map(e -> e.getField() + ": " + e.getDefaultMessage())
223
+ .toList());
224
+ return detail;
225
+ }
226
+ }
227
+ ```
228
+
229
+ ### Spring Security — JWT
230
+ ```java
231
+ @Configuration
232
+ @EnableWebSecurity
233
+ public class SecurityConfig {
234
+
235
+ @Bean
236
+ public SecurityFilterChain filterChain(HttpSecurity http, JwtAuthFilter jwtFilter) throws Exception {
237
+ return http
238
+ .csrf(AbstractHttpConfigurer::disable)
239
+ .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
240
+ .authorizeHttpRequests(auth -> auth
241
+ .requestMatchers("/api/v1/auth/**", "/actuator/health").permitAll()
242
+ .anyRequest().authenticated()
243
+ )
244
+ .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
245
+ .build();
246
+ }
247
+
248
+ @Bean
249
+ public PasswordEncoder passwordEncoder() {
250
+ return new BCryptPasswordEncoder();
251
+ }
252
+ }
253
+ ```
254
+
255
+ ### Testing
256
+ ```java
257
+ // @WebMvcTest — controller slice test (no full context)
258
+ @WebMvcTest(UserController.class)
259
+ class UserControllerTest {
260
+ @Autowired MockMvc mockMvc;
261
+ @MockitoBean UserService userService; // Spring Boot 3.4+: @MockitoBean
262
+
263
+ @Test
264
+ void createUser_returnsCreated() throws Exception {
265
+ given(userService.createUser(any())).willReturn(new UserDto(1L, "a@b.com", Instant.now()));
266
+
267
+ mockMvc.perform(post("/api/v1/users")
268
+ .contentType(MediaType.APPLICATION_JSON)
269
+ .content("""{"email":"a@b.com","password":"password123"}"""))
270
+ .andExpect(status().isCreated())
271
+ .andExpect(jsonPath("$.email").value("a@b.com"));
272
+ }
273
+ }
274
+
275
+ // @SpringBootTest — full integration test
276
+ @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
277
+ @Transactional
278
+ class UserServiceIntegrationTest {
279
+ @Autowired UserService userService;
280
+
281
+ @Test
282
+ void createUser_persistsToDatabase() {
283
+ var user = userService.createUser(new CreateUserRequest("a@b.com", "password123"));
284
+ assertThat(user.email()).isEqualTo("a@b.com");
285
+ assertThat(user.id()).isNotNull();
286
+ }
287
+ }
288
+ ```
289
+
290
+ ### Spring Cloud patterns
291
+ ```yaml
292
+ # Service discovery with Eureka
293
+ spring:
294
+ application:
295
+ name: user-service
296
+ eureka:
297
+ client:
298
+ service-url:
299
+ defaultZone: http://eureka-server:8761/eureka/
300
+
301
+ # Config server client
302
+ spring:
303
+ config:
304
+ import: configserver:http://config-server:8888
305
+ ```
306
+
307
+ ```java
308
+ // Feign client for inter-service calls
309
+ @FeignClient(name = "order-service")
310
+ public interface OrderClient {
311
+ @GetMapping("/api/v1/orders/user/{userId}")
312
+ List<OrderDto> getOrdersByUser(@PathVariable Long userId);
313
+ }
314
+ ```
315
+
316
+ ## Example
317
+
318
+ **User:** Build a Spring Boot REST API for a product catalogue with CRUD endpoints, PostgreSQL via JPA, bean validation, global error handling, and a `@WebMvcTest` for the GET endpoint.
319
+
320
+ **Expected output:**
321
+ - `Product` entity — `id`, `name`, `price` (BigDecimal), `stock`, `createdAt`
322
+ - `ProductDto` record + `CreateProductRequest` record with `@NotBlank` / `@Positive` validation
323
+ - `ProductRepository extends JpaRepository<Product, Long>`
324
+ - `ProductService` — `@Transactional(readOnly = true)` class-level, `@Transactional` on writes
325
+ - `ProductController` — CRUD at `/api/v1/products`, `Pageable` on GET list
326
+ - `GlobalExceptionHandler` — `NotFoundException` → 404, `MethodArgumentNotValidException` → 400 with field errors
327
+ - `ProductControllerTest` using `@WebMvcTest` + `@MockitoBean ProductService`
328
+
329
+ ---
330
+
331
+ > **Work with us:** Claudient is backed by [Uitbreiden](https://uitbreiden.com/) — we build AI products and B2B solutions with developer communities. [uitbreiden.com](https://uitbreiden.com/)