claudient 0.2.0 → 0.4.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.
- package/.claude-plugin/plugin.json +4 -2
- package/README.md +150 -103
- package/guides/agent-orchestration.md +1 -0
- package/guides/token-optimization.md +37 -0
- package/index.json +1801 -0
- package/package.json +5 -3
- package/scripts/build-index.js +139 -0
- package/scripts/cli.js +134 -1
- package/skills/backend/java/de/spring-boot.md +333 -0
- package/skills/backend/java/es/spring-boot.md +333 -0
- package/skills/backend/java/fr/spring-boot.md +333 -0
- package/skills/backend/java/nl/spring-boot.md +333 -0
- package/skills/backend/java/spring-boot.md +331 -0
- package/skills/productivity/caveman.md +70 -0
- package/skills/productivity/de/caveman.md +72 -0
- package/skills/productivity/es/caveman.md +72 -0
- package/skills/productivity/fr/caveman.md +72 -0
- package/skills/productivity/nl/caveman.md +72 -0
|
@@ -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/)
|