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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudient",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "The definitive Claude Code knowledge system — skills, agents, hooks, rules, and workflows.",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -34,12 +34,14 @@
34
34
  "guides/",
35
35
  ".claude-plugin/",
36
36
  "CONTEXT.md",
37
- "README.md"
37
+ "README.md",
38
+ "index.json"
38
39
  ],
39
40
  "engines": {
40
41
  "node": ">=18"
41
42
  },
42
43
  "scripts": {
43
- "list": "node scripts/cli.js list"
44
+ "list": "node scripts/cli.js list",
45
+ "build-index": "node scripts/build-index.js"
44
46
  }
45
47
  }
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env node
2
+ // build-index.js — generates index.json, a machine-readable manifest of all content
3
+
4
+ const fs = require('fs')
5
+ const path = require('path')
6
+
7
+ const ROOT = path.resolve(__dirname, '..')
8
+ const LANGS = ['fr', 'de', 'nl', 'es']
9
+ const SKILL_CATEGORIES = ['backend', 'devops-infra', 'data-ml', 'database', 'finance-payments', 'ai-engineering', 'productivity']
10
+
11
+ function getFiles(dir, ext = '.md') {
12
+ const results = []
13
+ if (!fs.existsSync(dir)) return results
14
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
15
+ const full = path.join(dir, entry.name)
16
+ if (entry.isDirectory()) results.push(...getFiles(full, ext))
17
+ else if (entry.name.endsWith(ext)) results.push(full)
18
+ }
19
+ return results
20
+ }
21
+
22
+ function relPath(full) {
23
+ return full.replace(ROOT + '/', '')
24
+ }
25
+
26
+ function isTranslation(filepath) {
27
+ return LANGS.some(l => filepath.includes(`/${l}/`))
28
+ }
29
+
30
+ function getLang(filepath) {
31
+ for (const l of LANGS) {
32
+ if (filepath.includes(`/${l}/`)) return l
33
+ }
34
+ return 'en'
35
+ }
36
+
37
+ function readTitle(filepath) {
38
+ try {
39
+ const first = fs.readFileSync(filepath, 'utf-8').split('\n').find(l => l.startsWith('# '))
40
+ return first ? first.replace(/^# /, '').trim() : path.basename(filepath, '.md')
41
+ } catch {
42
+ return path.basename(filepath, '.md')
43
+ }
44
+ }
45
+
46
+ const index = {
47
+ version: require(path.join(ROOT, 'package.json')).version,
48
+ generated: new Date().toISOString(),
49
+ skills: [],
50
+ agents: [],
51
+ hooks: [],
52
+ rules: [],
53
+ workflows: [],
54
+ prompts: [],
55
+ guides: [],
56
+ }
57
+
58
+ // Skills
59
+ for (const cat of SKILL_CATEGORIES) {
60
+ const catDir = path.join(ROOT, 'skills', cat)
61
+ for (const file of getFiles(catDir)) {
62
+ const rel = relPath(file)
63
+ const lang = getLang(rel)
64
+ const slug = rel.replace(/^skills\//, '').replace(/\.md$/, '').replace(/\/(fr|de|nl|es)\//, '/')
65
+ index.skills.push({
66
+ id: slug,
67
+ category: cat,
68
+ lang,
69
+ title: readTitle(file),
70
+ file: rel,
71
+ })
72
+ }
73
+ }
74
+
75
+ // Agents
76
+ for (const file of getFiles(path.join(ROOT, 'agents'))) {
77
+ const rel = relPath(file)
78
+ const lang = getLang(rel)
79
+ const slug = rel.replace(/^agents\//, '').replace(/\.md$/, '').replace(/\/(fr|de|nl|es)\//, '/')
80
+ index.agents.push({ id: slug, lang, title: readTitle(file), file: rel })
81
+ }
82
+
83
+ // Hooks (md docs)
84
+ for (const file of getFiles(path.join(ROOT, 'hooks'))) {
85
+ const rel = relPath(file)
86
+ const cat = rel.split('/')[1]
87
+ const slug = path.basename(file, '.md')
88
+ index.hooks.push({ id: `${cat}/${slug}`, category: cat, title: readTitle(file), file: rel })
89
+ }
90
+
91
+ // Rules
92
+ for (const file of getFiles(path.join(ROOT, 'rules'))) {
93
+ const rel = relPath(file)
94
+ const lang = getLang(rel)
95
+ const slug = rel.replace(/^rules\//, '').replace(/\.md$/, '').replace(/\/(fr|de|nl|es)\//, '/')
96
+ index.rules.push({ id: slug, lang, title: readTitle(file), file: rel })
97
+ }
98
+
99
+ // Workflows
100
+ for (const file of getFiles(path.join(ROOT, 'workflows'))) {
101
+ const rel = relPath(file)
102
+ const lang = getLang(rel)
103
+ const slug = path.basename(file, '.md')
104
+ index.workflows.push({ id: slug, lang, title: readTitle(file), file: rel })
105
+ }
106
+
107
+ // Prompts
108
+ for (const file of getFiles(path.join(ROOT, 'prompts'))) {
109
+ const rel = relPath(file)
110
+ const lang = getLang(rel)
111
+ const cat = rel.split('/')[1]
112
+ const slug = `${cat}/${path.basename(file, '.md')}`
113
+ index.prompts.push({ id: slug.replace(/\/(fr|de|nl|es)\//, '/'), category: cat, lang, title: readTitle(file), file: rel })
114
+ }
115
+
116
+ // Guides
117
+ for (const file of getFiles(path.join(ROOT, 'guides'))) {
118
+ const rel = relPath(file)
119
+ const lang = getLang(rel)
120
+ const slug = path.basename(file, '.md')
121
+ index.guides.push({ id: slug, lang, title: readTitle(file), file: rel })
122
+ }
123
+
124
+ // Summary counts (English only)
125
+ const en = (arr) => arr.filter(i => i.lang === 'en' || !i.lang)
126
+ index.summary = {
127
+ skills: en(index.skills).length,
128
+ agents: en(index.agents).length,
129
+ hooks: index.hooks.length,
130
+ rules: en(index.rules).length,
131
+ workflows: en(index.workflows).length,
132
+ prompts: en(index.prompts).length,
133
+ guides: en(index.guides).length,
134
+ languages: ['en', ...LANGS],
135
+ }
136
+
137
+ const outPath = path.join(ROOT, 'index.json')
138
+ fs.writeFileSync(outPath, JSON.stringify(index, null, 2) + '\n')
139
+ console.log(`Generated index.json — ${index.summary.skills} skills, ${index.summary.agents} agents, ${index.summary.hooks} hooks`)
package/scripts/cli.js CHANGED
@@ -28,9 +28,10 @@ function usage() {
28
28
  claudient — Claude Code knowledge system
29
29
 
30
30
  Usage:
31
+ npx claudient init Interactive first-run setup
31
32
  npx claudient add skills [category] [--lang <lang>]
32
33
  npx claudient add agents
33
- npx claudient add rules
34
+ npx claudient add rules [--write]
34
35
  npx claudient add hooks
35
36
  npx claudient add all [--lang <lang>]
36
37
  npx claudient remove skills [category]
@@ -393,6 +394,135 @@ function listCommand(type) {
393
394
  }
394
395
  }
395
396
 
397
+ // ── Init (interactive first-run setup) ───────────────────────────────────────
398
+
399
+ async function initCommand() {
400
+ const readline = require('readline')
401
+
402
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
403
+ const ask = (q) => new Promise(resolve => rl.question(q, resolve))
404
+
405
+ const BOLD = '\x1b[1m'
406
+ const ORANGE = '\x1b[33m'
407
+ const GREEN = '\x1b[32m'
408
+ const DIM = '\x1b[2m'
409
+ const RESET = '\x1b[0m'
410
+
411
+ console.log(`
412
+ ${BOLD}╔══════════════════════════════════════════════╗
413
+ ║ CLAUDIENT SETUP ║
414
+ ║ The Claude Code Knowledge System ║
415
+ ╚══════════════════════════════════════════════╝${RESET}
416
+ `)
417
+
418
+ // Check Claude Code installed
419
+ if (!fs.existsSync(CLAUDE_DIR)) {
420
+ console.error(`${ORANGE}⚠ ~/.claude not found.${RESET}`)
421
+ console.error(` Claude Code must be installed first: https://claude.ai/code\n`)
422
+ rl.close()
423
+ process.exit(1)
424
+ }
425
+ console.log(`${GREEN}✓ Claude Code detected at ${CLAUDE_DIR}${RESET}\n`)
426
+
427
+ const summary = { skills: [], agents: false, hooks: false, rules: false, lang: 'en' }
428
+
429
+ // 1. Language
430
+ console.log(`${BOLD}Step 1/5 — Language${RESET}`)
431
+ console.log(' Available: en, fr, de, nl, es')
432
+ const langInput = (await ask(' Which language? [en] ')).trim().toLowerCase() || 'en'
433
+ summary.lang = SUPPORTED_LANGS.includes(langInput) ? langInput : 'en'
434
+ console.log(` → ${summary.lang}\n`)
435
+
436
+ // 2. Skill categories
437
+ console.log(`${BOLD}Step 2/5 — Skills${RESET}`)
438
+ SKILL_CATEGORIES.forEach((cat, i) => console.log(` ${i + 1}. ${cat}`))
439
+ console.log(' a. All categories')
440
+ console.log(' 0. Skip skills')
441
+ const catInput = (await ask(' Select categories (comma-separated numbers, or a/0): ')).trim()
442
+
443
+ if (catInput === '0') {
444
+ console.log(' → Skipping skills\n')
445
+ } else if (catInput === 'a' || catInput === '') {
446
+ summary.skills = [...SKILL_CATEGORIES]
447
+ console.log(` → All categories selected\n`)
448
+ } else {
449
+ const nums = catInput.split(',').map(n => parseInt(n.trim(), 10)).filter(n => n >= 1 && n <= SKILL_CATEGORIES.length)
450
+ summary.skills = nums.map(n => SKILL_CATEGORIES[n - 1])
451
+ console.log(` → Selected: ${summary.skills.join(', ')}\n`)
452
+ }
453
+
454
+ // 3. Agents
455
+ console.log(`${BOLD}Step 3/5 — Agents${RESET}`)
456
+ console.log(' 6 subagent definitions: Planner, Architect, Code Reviewer, Security, Build Resolvers')
457
+ const agentsInput = (await ask(' Install agents? [Y/n] ')).trim().toLowerCase()
458
+ summary.agents = agentsInput !== 'n'
459
+ console.log(` → ${summary.agents ? 'Yes' : 'No'}\n`)
460
+
461
+ // 4. Hooks
462
+ console.log(`${BOLD}Step 4/5 — Hooks${RESET}`)
463
+ console.log(' 7 shell scripts: safety guards, auto-formatter, audit log, cost tracker, session helpers')
464
+ const hooksInput = (await ask(' Install hooks? [Y/n] ')).trim().toLowerCase()
465
+ summary.hooks = hooksInput !== 'n'
466
+ console.log(` → ${summary.hooks ? 'Yes' : 'No'}\n`)
467
+
468
+ // 5. Rules
469
+ console.log(`${BOLD}Step 5/5 — Rules${RESET}`)
470
+ console.log(' 8 rule sets: coding style, git, security, testing, performance, Python, TypeScript, Go')
471
+ const rulesInput = (await ask(' Add rules to ./CLAUDE.md? [Y/n] ')).trim().toLowerCase()
472
+ summary.rules = rulesInput !== 'n'
473
+ console.log(` → ${summary.rules ? 'Yes' : 'No'}\n`)
474
+
475
+ rl.close()
476
+
477
+ // Confirm
478
+ console.log(`${BOLD}Summary${RESET}`)
479
+ console.log(` Language : ${summary.lang}`)
480
+ console.log(` Skills : ${summary.skills.length ? summary.skills.join(', ') : 'none'}`)
481
+ console.log(` Agents : ${summary.agents ? 'yes' : 'no'}`)
482
+ console.log(` Hooks : ${summary.hooks ? 'yes' : 'no'}`)
483
+ console.log(` Rules : ${summary.rules ? 'append to ./CLAUDE.md' : 'no'}`)
484
+ console.log()
485
+
486
+ // Execute
487
+ if (summary.skills.length) {
488
+ for (const cat of summary.skills) {
489
+ console.log(`Installing ${cat} skills...`)
490
+ addSkills(cat, summary.lang === 'en' ? null : summary.lang)
491
+ }
492
+ console.log()
493
+ }
494
+
495
+ if (summary.agents) {
496
+ console.log('Installing agents...')
497
+ addAgents()
498
+ console.log()
499
+ }
500
+
501
+ if (summary.hooks) {
502
+ console.log('Installing hooks...')
503
+ addHooks()
504
+ console.log()
505
+ }
506
+
507
+ if (summary.rules) {
508
+ console.log('Adding rules to ./CLAUDE.md...')
509
+ addRulesWrite()
510
+ console.log()
511
+ }
512
+
513
+ console.log(`${GREEN}${BOLD}✓ Claudient setup complete!${RESET}`)
514
+ console.log()
515
+ console.log('Next steps:')
516
+ if (summary.hooks) {
517
+ console.log(` 1. Add hook entries to .claude/settings.json`)
518
+ console.log(` See: https://github.com/Claudient/Claudient/tree/main/hooks`)
519
+ }
520
+ console.log(` 2. Restart Claude Code to activate all installed content`)
521
+ console.log(` 3. Try a skill — type /fastapi or /kubernetes in Claude Code`)
522
+ console.log()
523
+ console.log(` Full docs: https://github.com/Claudient/Claudient`)
524
+ }
525
+
396
526
  function getFiles(dir, prefix = '') {
397
527
  const results = []
398
528
  if (!fs.existsSync(dir)) return results
@@ -460,6 +590,9 @@ switch (command) {
460
590
  case 'list':
461
591
  listCommand(positional[0])
462
592
  break
593
+ case 'init':
594
+ initCommand().catch(err => { console.error(err); process.exit(1) })
595
+ break
463
596
  case 'help':
464
597
  case '--help':
465
598
  case '-h':
@@ -0,0 +1,333 @@
1
+ > 🇩🇪 Deutsche Version. [Englische Version](../spring-boot.md).
2
+
3
+ # Spring Boot Skill
4
+
5
+ ## Wann aktivieren
6
+ - Aufbau einer Spring Boot REST API oder eines Microservices
7
+ - Einrichtung von Spring Data JPA mit Hibernate und PostgreSQL/MySQL
8
+ - Konfiguration von Spring Security (JWT, OAuth2, sitzungsbasierte Authentifizierung)
9
+ - Schreiben von Spring Boot Tests (Unit-Tests mit Mockito, Integrationstests mit TestRestTemplate oder MockMvc)
10
+ - Einrichtung von Spring Cloud (Service Discovery, Config Server, API Gateway)
11
+ - Implementierung asynchroner Verarbeitung mit `@Async` oder Spring Events
12
+ - Konfiguration von Profilen, Properties und externalisierter Konfiguration
13
+ - Schreiben benutzerdefinierter Spring Boot Starters oder Auto-Konfigurationen
14
+
15
+ ## Wann NICHT verwenden
16
+ - Quarkus- oder Micronaut-Projekte — unterschiedliche Dependency-Injection- und Konfigurationsmodelle
17
+ - Reines Java ohne Spring — der Overhead ist für einfache Skripte nicht gerechtfertigt
18
+ - Android-Projekte — vollständig anderes Ökosystem
19
+ - Jakarta EE/WildFly — anderes Application-Server-Modell
20
+
21
+ ## Anweisungen
22
+
23
+ ### Projektstruktur
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
+ ### Anwendungs-Einstiegspunkt
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 Struktur
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 und 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-Schicht
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
+ ### Controller-Schicht
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 mit 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
+ ### Globaler Exception-Handler
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 — Controller-Slice-Test (kein vollständiger Kontext)
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 — vollständiger Integrationstest
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 Muster
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
+ ## Beispiel
319
+
320
+ **Nutzer:** Erstelle eine Spring Boot REST API für einen Produktkatalog mit CRUD-Endpunkten, PostgreSQL über JPA, Bean-Validierung, globalem Fehler-Handling und einem `@WebMvcTest` für den GET-Endpunkt.
321
+
322
+ **Erwartete Ausgabe:**
323
+ - `Product` Entity — `id`, `name`, `price` (BigDecimal), `stock`, `createdAt`
324
+ - `ProductDto` Record + `CreateProductRequest` Record mit `@NotBlank` / `@Positive` Validierung
325
+ - `ProductRepository extends JpaRepository<Product, Long>`
326
+ - `ProductService` — `@Transactional(readOnly = true)` auf Klassenebene, `@Transactional` für Schreiboperationen
327
+ - `ProductController` — CRUD unter `/api/v1/products`, `Pageable` auf GET-Liste
328
+ - `GlobalExceptionHandler` — `NotFoundException` → 404, `MethodArgumentNotValidException` → 400 mit Feldfehlern
329
+ - `ProductControllerTest` mit `@WebMvcTest` + `@MockitoBean ProductService`
330
+
331
+ ---
332
+
333
+ > **Arbeite mit uns:** Claudient wird von [Uitbreiden](https://uitbreiden.com/) unterstützt — wir entwickeln KI-Produkte und B2B-Lösungen mit Entwickler-Communities. [uitbreiden.com](https://uitbreiden.com/)