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.
- 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 +1745 -0
- package/package.json +5 -3
- package/scripts/build-index.js +139 -0
- 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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claudient",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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`)
|
|
@@ -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/)
|