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
+ > 🇪🇸 Versión en español. [Versión en inglés](../spring-boot.md).
2
+
3
+ # Skill Spring Boot
4
+
5
+ ## Cuándo activar
6
+ - Construyendo una API REST o microservicio con Spring Boot
7
+ - Configurando Spring Data JPA con Hibernate y PostgreSQL/MySQL
8
+ - Configurando Spring Security (JWT, OAuth2, autenticación basada en sesión)
9
+ - Escribiendo pruebas para Spring Boot (unitarias con Mockito, de integración con TestRestTemplate o MockMvc)
10
+ - Configurando Spring Cloud (descubrimiento de servicios, servidor de configuración, API gateway)
11
+ - Implementando procesamiento asíncrono con `@Async` o Spring Events
12
+ - Configurando perfiles, propiedades y configuración externalizada
13
+ - Escribiendo starters personalizados de Spring Boot o auto-configuraciones
14
+
15
+ ## Cuándo NO usar
16
+ - Proyectos con Quarkus o Micronaut — modelos de inyección de dependencias y configuración distintos
17
+ - Java simple sin Spring — la sobrecarga no está justificada para scripts simples
18
+ - Proyectos Android — ecosistema completamente diferente
19
+ - Jakarta EE/WildFly — modelo de servidor de aplicaciones distinto
20
+
21
+ ## Instrucciones
22
+
23
+ ### Estructura del proyecto
24
+ ```
25
+ src/
26
+ ├── main/
27
+ │ ├── java/com/example/app/
28
+ │ │ ├── AppApplication.java # @SpringBootApplication entry point
29
+ │ │ ├── config/ # @Configuration beans
30
+ │ │ ├── controller/ # @RestController — HTTP layer only
31
+ │ │ ├── service/ # Business logic — @Service
32
+ │ │ ├── repository/ # @Repository — data access
33
+ │ │ ├── domain/ # @Entity models
34
+ │ │ ├── dto/ # Request/response shapes (records)
35
+ │ │ ├── exception/ # @ControllerAdvice error handling
36
+ │ │ └── security/ # Security config and filters
37
+ │ └── resources/
38
+ │ ├── application.yml # Base config
39
+ │ ├── application-dev.yml # Dev overrides
40
+ │ └── application-prod.yml # Prod overrides
41
+ └── test/
42
+ └── java/com/example/app/
43
+ ├── controller/ # MockMvc / @WebMvcTest
44
+ ├── service/ # Unit tests with Mockito
45
+ └── integration/ # @SpringBootTest full context
46
+ ```
47
+
48
+ ### Punto de entrada de la aplicación
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
+ ### Estructura de application.yml
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 # Never 'create' or 'update' in production
70
+ show-sql: false
71
+ properties:
72
+ hibernate:
73
+ format_sql: true
74
+
75
+ server:
76
+ port: ${PORT:8080}
77
+
78
+ # Custom properties — always use @ConfigurationProperties, never @Value for groups
79
+ app:
80
+ jwt:
81
+ secret: ${JWT_SECRET}
82
+ expiration-ms: 86400000
83
+ ```
84
+
85
+ ### Entidad y 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
+ ### Capa de servicio
127
+ ```java
128
+ @Service
129
+ @Transactional(readOnly = true) // Default read-only; override for writes
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 // Override: this method writes
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
+ ### Capa de controlador
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
+ ### DTOs con 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
+ ### Manejador global de excepciones
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
+ ### Pruebas
258
+ ```java
259
+ // @WebMvcTest — prueba de slice del controlador (sin contexto completo)
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 — prueba de integración completa
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
+ ### Patrones de Spring Cloud
293
+ ```yaml
294
+ # Service discovery with 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 for inter-service calls
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
+ ## Ejemplo
319
+
320
+ **Usuario:** Construye una API REST con Spring Boot para un catálogo de productos con endpoints CRUD, PostgreSQL vía JPA, validación de beans, manejo global de errores y un `@WebMvcTest` para el endpoint GET.
321
+
322
+ **Resultado esperado:**
323
+ - Entidad `Product` — `id`, `name`, `price` (BigDecimal), `stock`, `createdAt`
324
+ - Record `ProductDto` + record `CreateProductRequest` con validaciones `@NotBlank` / `@Positive`
325
+ - `ProductRepository extends JpaRepository<Product, Long>`
326
+ - `ProductService` — clase con `@Transactional(readOnly = true)` a nivel de clase, `@Transactional` en escrituras
327
+ - `ProductController` — CRUD en `/api/v1/products`, `Pageable` en el listado GET
328
+ - `GlobalExceptionHandler` — `NotFoundException` → 404, `MethodArgumentNotValidException` → 400 con errores de campo
329
+ - `ProductControllerTest` usando `@WebMvcTest` + `@MockitoBean ProductService`
330
+
331
+ ---
332
+
333
+ > **Trabaja con nosotros:** Claudient está respaldado por [Uitbreiden](https://uitbreiden.com/) — construimos productos de IA y soluciones B2B con comunidades de desarrolladores. [uitbreiden.com](https://uitbreiden.com/)
@@ -0,0 +1,333 @@
1
+ > 🇫🇷 Version française. [English version](../spring-boot.md).
2
+
3
+ # Skill Spring Boot
4
+
5
+ ## Quand activer
6
+ - Construire une API REST Spring Boot ou un microservice
7
+ - Configurer Spring Data JPA avec Hibernate et PostgreSQL/MySQL
8
+ - Configurer Spring Security (JWT, OAuth2, authentification par session)
9
+ - Écrire des tests Spring Boot (unitaires avec Mockito, d'intégration avec TestRestTemplate ou MockMvc)
10
+ - Mettre en place Spring Cloud (découverte de services, serveur de configuration, API gateway)
11
+ - Implémenter le traitement asynchrone avec `@Async` ou les événements Spring
12
+ - Configurer les profils, les propriétés et la configuration externalisée
13
+ - Écrire des starters Spring Boot personnalisés ou des auto-configurations
14
+
15
+ ## Quand NE PAS utiliser
16
+ - Projets Quarkus ou Micronaut — modèles d'injection de dépendances et de configuration différents
17
+ - Java sans Spring — la surcharge n'est pas justifiée pour des scripts simples
18
+ - Projets Android — écosystème entièrement différent
19
+ - Jakarta EE/WildFly — modèle de serveur d'applications différent
20
+
21
+ ## Instructions
22
+
23
+ ### Structure du projet
24
+ ```
25
+ src/
26
+ ├── main/
27
+ │ ├── java/com/example/app/
28
+ │ │ ├── AppApplication.java # @SpringBootApplication entry point
29
+ │ │ ├── config/ # @Configuration beans
30
+ │ │ ├── controller/ # @RestController — HTTP layer only
31
+ │ │ ├── service/ # Business logic — @Service
32
+ │ │ ├── repository/ # @Repository — data access
33
+ │ │ ├── domain/ # @Entity models
34
+ │ │ ├── dto/ # Request/response shapes (records)
35
+ │ │ ├── exception/ # @ControllerAdvice error handling
36
+ │ │ └── security/ # Security config and filters
37
+ │ └── resources/
38
+ │ ├── application.yml # Base config
39
+ │ ├── application-dev.yml # Dev overrides
40
+ │ └── application-prod.yml # Prod overrides
41
+ └── test/
42
+ └── java/com/example/app/
43
+ ├── controller/ # MockMvc / @WebMvcTest
44
+ ├── service/ # Unit tests with Mockito
45
+ └── integration/ # @SpringBootTest full context
46
+ ```
47
+
48
+ ### Point d'entrée de l'application
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
+ ### Structure de application.yml
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 # Never 'create' or 'update' in production
70
+ show-sql: false
71
+ properties:
72
+ hibernate:
73
+ format_sql: true
74
+
75
+ server:
76
+ port: ${PORT:8080}
77
+
78
+ # Custom properties — always use @ConfigurationProperties, never @Value for groups
79
+ app:
80
+ jwt:
81
+ secret: ${JWT_SECRET}
82
+ expiration-ms: 86400000
83
+ ```
84
+
85
+ ### Entity et 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
+ ### Couche service
127
+ ```java
128
+ @Service
129
+ @Transactional(readOnly = true) // Default read-only; override for writes
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 // Override: this method writes
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
+ ### Couche contrôleur
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
+ ### DTOs avec des 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
+ ### Gestionnaire d'exceptions global
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
+ ### Tests
258
+ ```java
259
+ // @WebMvcTest — test de tranche contrôleur (sans contexte complet)
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 — test d'intégration complet
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
+ ### Patterns Spring Cloud
293
+ ```yaml
294
+ # Service discovery with 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 for inter-service calls
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
+ ## Exemple
319
+
320
+ **Utilisateur :** Construire une API REST Spring Boot pour un catalogue de produits avec des endpoints CRUD, PostgreSQL via JPA, validation des beans, gestion globale des erreurs, et un `@WebMvcTest` pour l'endpoint GET.
321
+
322
+ **Résultat attendu :**
323
+ - Entity `Product` — `id`, `name`, `price` (BigDecimal), `stock`, `createdAt`
324
+ - Record `ProductDto` + record `CreateProductRequest` avec validation `@NotBlank` / `@Positive`
325
+ - `ProductRepository extends JpaRepository<Product, Long>`
326
+ - `ProductService` — classe avec `@Transactional(readOnly = true)`, `@Transactional` sur les écritures
327
+ - `ProductController` — CRUD sur `/api/v1/products`, `Pageable` sur la liste GET
328
+ - `GlobalExceptionHandler` — `NotFoundException` → 404, `MethodArgumentNotValidException` → 400 avec les erreurs de champ
329
+ - `ProductControllerTest` utilisant `@WebMvcTest` + `@MockitoBean ProductService`
330
+
331
+ ---
332
+
333
+ > **Travaillez avec nous :** Claudient est soutenu par [Uitbreiden](https://uitbreiden.com/) — nous construisons des produits IA et des solutions B2B avec des communautés de développeurs. [uitbreiden.com](https://uitbreiden.com/)