@booklib/skills 1.4.0 → 1.5.1

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.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # booklib-ai/skills
2
2
 
3
- 21 AI agent skills grounded in canonical programming books. Each skill packages expert practices from a specific book into reusable instructions that Claude and other AI agents can apply to code generation, code review, and design decisions.
3
+ 22 AI agent skills grounded in canonical programming books. Each skill packages expert practices from a specific book into reusable instructions that Claude and other AI agents can apply to code generation, code review, and design decisions.
4
4
 
5
5
  ## Quick Install
6
6
 
@@ -25,6 +25,7 @@ npx skills add booklib-ai/skills --all -g
25
25
  | `kotlin-in-action` | Kotlin in Action |
26
26
  | `programming-with-rust` | Programming with Rust — Donis Marshall |
27
27
  | `rust-in-action` | Rust in Action — Tim McNamara |
28
+ | `spring-boot-in-action` | Spring Boot in Action — Craig Walls |
28
29
  | `lean-startup` | The Lean Startup — Eric Ries |
29
30
  | `microservices-patterns` | Microservices Patterns — Chris Richardson |
30
31
  | `refactoring-ui` | Refactoring UI — Adam Wathan & Steve Schoger |
@@ -54,4 +55,4 @@ See [CONTRIBUTING.md](./CONTRIBUTING.md) for how to add a new skill. Each skill
54
55
  npx @booklib/skills check <skill-name>
55
56
  ```
56
57
 
57
- All 21 existing skills are at Platinum (13/13 checks).
58
+ All 22 existing skills are at Platinum (13/13 checks).
package/README.md CHANGED
@@ -134,6 +134,7 @@ This means skills compose: `skill-router` acts as an orchestrator that picks the
134
134
  | 🔷 [effective-typescript](./skills/effective-typescript/) | TypeScript best practices from Dan Vanderkam's *Effective TypeScript* — type system, type design, avoiding any, type declarations, and migration |
135
135
  | 🦀 [programming-with-rust](./skills/programming-with-rust/) | Rust practices from Donis Marshall's *Programming with Rust* — ownership, borrowing, lifetimes, error handling, traits, and fearless concurrency |
136
136
  | ⚙️ [rust-in-action](./skills/rust-in-action/) | Systems programming from Tim McNamara's *Rust in Action* — smart pointers, endianness, memory, file formats, TCP networking, concurrency, and OS fundamentals |
137
+ | 🌱 [spring-boot-in-action](./skills/spring-boot-in-action/) | Spring Boot best practices from Craig Walls' *Spring Boot in Action* — auto-configuration, starters, externalized config, profiles, testing with MockMvc, Actuator, and deployment |
137
138
  | ⚡ [kotlin-in-action](./skills/kotlin-in-action/) | Practices from *Kotlin in Action* (2nd Ed) — functions, classes, lambdas, nullability, and coroutines |
138
139
  | 🚀 [lean-startup](./skills/lean-startup/) | Practices from Eric Ries' *The Lean Startup* — MVP testing, validated learning, Build-Measure-Learn loop, and pivots |
139
140
  | 🔧 [microservices-patterns](./skills/microservices-patterns/) | Expert guidance on microservices patterns from Chris Richardson's *Microservices Patterns* — decomposition, sagas, API gateways, event sourcing, CQRS, and service mesh |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@booklib/skills",
3
- "version": "1.4.0",
3
+ "version": "1.5.1",
4
4
  "description": "Book knowledge distilled into structured AI skills for Claude Code and other AI assistants",
5
5
  "bin": {
6
6
  "skills": "bin/skills.js"
@@ -29,6 +29,7 @@
29
29
  "Recognize that this code is already applying Effective TypeScript principles correctly",
30
30
  "Acknowledge Item 37 (branded OrderId), Item 33 (OrderStatus literal union), Item 28/32 (tagged union OrderResult), Item 17 (readonly fields), Item 42 (unknown from JSON), Item 40 (assertion inside well-typed function), Item 25 (async/await), Item 48 (TSDoc)",
31
31
  "Do NOT manufacture issues — the code is well-typed",
32
+ "At most note: raw as Order on the last line is a narrowly scoped assertion (Item 40) — acceptable inside a well-typed wrapper, but mention that a runtime validator (e.g. zod) would catch malformed API responses that TypeScript cannot",
32
33
  "At most offer minor suggestions, clearly marked as optional polish"
33
34
  ]
34
35
  }
@@ -0,0 +1,169 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ review.py — Pre-analysis script for Effective TypeScript reviews.
4
+ Usage: python review.py <file.ts|file.tsx>
5
+
6
+ Scans a TypeScript source file for anti-patterns from the book's 62 items:
7
+ any usage, type assertions, object wrapper types, non-null assertions,
8
+ missing strict mode, interface-of-unions, plain string types, and more.
9
+ """
10
+
11
+ import re
12
+ import sys
13
+ from pathlib import Path
14
+
15
+
16
+ CHECKS = [
17
+ (
18
+ r":\s*any\b",
19
+ "Item 5/38: any type annotation",
20
+ "replace with a specific type, generic parameter, or unknown for truly unknown values",
21
+ ),
22
+ (
23
+ r"\bas\s+any\b",
24
+ "Item 38/40: 'as any' assertion",
25
+ "scope 'as any' as narrowly as possible — hide inside a well-typed wrapper function; prefer 'as unknown as T' for safer double assertion",
26
+ ),
27
+ (
28
+ r"\bString\b|\bNumber\b|\bBoolean\b|\bObject\b|\bSymbol\b",
29
+ "Item 10: Object wrapper type (String/Number/Boolean)",
30
+ "use primitive types: string, number, boolean — never the wrapper class types",
31
+ ),
32
+ (
33
+ r"!\.",
34
+ "Item 28/31: Non-null assertion (!).",
35
+ "non-null assertions are usually a symptom of an imprecise type — fix the type instead; consider optional chaining (?.) or a type guard",
36
+ ),
37
+ (
38
+ r"@ts-ignore|@ts-nocheck",
39
+ "Item 38: @ts-ignore suppresses type errors",
40
+ "fix the underlying type issue; if unavoidable use @ts-expect-error with a comment explaining why",
41
+ ),
42
+ (
43
+ r"function\s+\w+[^{]*\{[^}]{0,20}\}",
44
+ None, # skip — too noisy
45
+ None,
46
+ ),
47
+ (
48
+ r"interface\s+\w+\s*\{[^}]*\?[^}]*\?[^}]*\}",
49
+ "Item 32: Interface with multiple optional fields",
50
+ "multiple optional fields that have implicit relationships suggest an interface-of-unions — convert to a tagged discriminated union",
51
+ ),
52
+ (
53
+ r"param(?:eter)?\s*:\s*string(?!\s*[|&])",
54
+ "Item 33: Plain string parameter",
55
+ "consider a string literal union if the parameter has a finite set of valid values (e.g. 'asc' | 'desc')",
56
+ ),
57
+ (
58
+ r"\.json\(\)\s*as\s+\w",
59
+ "Item 9/40: Direct type assertion on .json()",
60
+ "assign to unknown first, then narrow: 'const raw: unknown = await res.json()' — assertion inside a well-typed wrapper is acceptable (Item 40)",
61
+ ),
62
+ (
63
+ r"Promise<any>",
64
+ "Item 38: Promise<any> return type",
65
+ "replace with Promise<unknown> or a concrete type — Promise<any> disables type checking on the resolved value",
66
+ ),
67
+ ]
68
+
69
+
70
+ def scan(source: str) -> list[dict]:
71
+ findings = []
72
+ lines = source.splitlines()
73
+ for lineno, line in enumerate(lines, start=1):
74
+ stripped = line.strip()
75
+ if stripped.startswith("//") or stripped.startswith("*"):
76
+ continue
77
+ for pattern, label, advice in CHECKS:
78
+ if label is None:
79
+ continue
80
+ if re.search(pattern, line):
81
+ findings.append({
82
+ "line": lineno,
83
+ "text": line.rstrip(),
84
+ "label": label,
85
+ "advice": advice,
86
+ })
87
+ return findings
88
+
89
+
90
+ def check_strict(source: str) -> bool:
91
+ """Returns True if this looks like a tsconfig with strict mode enabled."""
92
+ return bool(re.search(r'"strict"\s*:\s*true', source))
93
+
94
+
95
+ def sep(char="-", width=70) -> str:
96
+ return char * width
97
+
98
+
99
+ def main() -> None:
100
+ if len(sys.argv) < 2:
101
+ print("Usage: python review.py <file.ts|file.tsx>")
102
+ sys.exit(1)
103
+
104
+ path = Path(sys.argv[1])
105
+ if not path.exists():
106
+ print(f"Error: file not found: {path}")
107
+ sys.exit(1)
108
+
109
+ if path.suffix.lower() not in (".ts", ".tsx", ".json"):
110
+ print(f"Warning: expected .ts/.tsx, got '{path.suffix}' — continuing anyway")
111
+
112
+ source = path.read_text(encoding="utf-8", errors="replace")
113
+
114
+ # Special case: tsconfig.json
115
+ if path.name == "tsconfig.json":
116
+ print(sep("="))
117
+ print("EFFECTIVE TYPESCRIPT — TSCONFIG CHECK")
118
+ print(sep("="))
119
+ if check_strict(source):
120
+ print(" [OK] strict: true is enabled (Item 2)")
121
+ else:
122
+ print(" [!] strict: true is NOT enabled — Item 2: always enable strict mode")
123
+ print(" Add: \"strict\": true to compilerOptions")
124
+ print()
125
+ print(sep("="))
126
+ return
127
+
128
+ findings = scan(source)
129
+ groups: dict[str, list] = {}
130
+ for f in findings:
131
+ groups.setdefault(f["label"], []).append(f)
132
+
133
+ print(sep("="))
134
+ print("EFFECTIVE TYPESCRIPT — PRE-REVIEW REPORT")
135
+ print(sep("="))
136
+ print(f"File : {path}")
137
+ print(f"Lines : {len(source.splitlines())}")
138
+ print(f"Issues : {len(findings)} potential violations across {len(groups)} categories")
139
+ print()
140
+
141
+ if not findings:
142
+ print(" [OK] No common Effective TypeScript anti-patterns detected.")
143
+ print()
144
+ else:
145
+ for label, items in groups.items():
146
+ print(sep())
147
+ print(f" {label} ({len(items)} occurrence{'s' if len(items) != 1 else ''})")
148
+ print(sep())
149
+ print(f" Advice: {items[0]['advice']}")
150
+ print()
151
+ for item in items[:5]:
152
+ print(f" line {item['line']:>4}: {item['text'][:100]}")
153
+ if len(items) > 5:
154
+ print(f" ... and {len(items) - 5} more")
155
+ print()
156
+
157
+ severity = (
158
+ "HIGH" if len(findings) >= 5
159
+ else "MEDIUM" if len(findings) >= 2
160
+ else "LOW" if findings
161
+ else "NONE"
162
+ )
163
+ print(sep("="))
164
+ print(f"SEVERITY: {severity} | Key items: Item 2 (strict), Item 5/38 (any/unknown), Item 9 (assertions), Item 28/32 (tagged unions), Item 33 (literal types)")
165
+ print(sep("="))
166
+
167
+
168
+ if __name__ == "__main__":
169
+ main()
@@ -0,0 +1,142 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ review.py — Pre-analysis script for Programming with Rust reviews.
4
+ Usage: python review.py <file.rs>
5
+
6
+ Scans a Rust source file for anti-patterns from the book:
7
+ unwrap misuse, unnecessary cloning, unsafe shared state, manual index loops,
8
+ missing Result return types, static mut, and more.
9
+ """
10
+
11
+ import re
12
+ import sys
13
+ from pathlib import Path
14
+
15
+
16
+ CHECKS = [
17
+ (
18
+ r"\.unwrap\(\)",
19
+ "Ch 12: .unwrap()",
20
+ "panics on failure in production paths — use ?, .expect(\"reason\"), or match",
21
+ ),
22
+ (
23
+ r"\.clone\(\)",
24
+ "Ch 8: .clone()",
25
+ "verify cloning is necessary — prefer borrowing (&T or &mut T) to avoid heap allocation",
26
+ ),
27
+ (
28
+ r"static\s+mut\s+\w+",
29
+ "Ch 19: static mut",
30
+ "data race risk — replace with Arc<Mutex<T>> or std::sync::atomic types",
31
+ ),
32
+ (
33
+ r"unsafe\s*\{",
34
+ "Ch 20: unsafe block",
35
+ "minimize unsafe scope; add a // SAFETY: comment explaining the invariant being upheld",
36
+ ),
37
+ (
38
+ r"for\s+\w+\s+in\s+0\s*\.\.\s*\w+\.len\(\)",
39
+ "Ch 6: Manual index loop",
40
+ "use iterator adapters: for item in &collection, or .iter().enumerate() if index is needed",
41
+ ),
42
+ (
43
+ r"\bpanic!\s*\(",
44
+ "Ch 12: panic!()",
45
+ "panics should be reserved for unrecoverable programmer errors — use Result<T, E> for recoverable failures",
46
+ ),
47
+ (
48
+ r"Box<dyn\s+\w+>",
49
+ "Ch 17: dyn Trait (dynamic dispatch)",
50
+ "prefer impl Trait for static dispatch (zero-cost) unless you need a heterogeneous collection",
51
+ ),
52
+ (
53
+ r"Rc\s*::\s*(new|clone)\b",
54
+ "Ch 19: Rc usage",
55
+ "Rc is not Send — if shared across threads, use Arc instead",
56
+ ),
57
+ (
58
+ r"\.expect\s*\(\s*\)",
59
+ "Ch 12: .expect() with empty string",
60
+ "add a meaningful reason: .expect(\"invariant: config is always loaded before this point\")",
61
+ ),
62
+ ]
63
+
64
+
65
+ def scan(source: str) -> list[dict]:
66
+ findings = []
67
+ lines = source.splitlines()
68
+ for lineno, line in enumerate(lines, start=1):
69
+ stripped = line.strip()
70
+ if stripped.startswith("//"):
71
+ continue
72
+ for pattern, label, advice in CHECKS:
73
+ if re.search(pattern, line):
74
+ findings.append({
75
+ "line": lineno,
76
+ "text": line.rstrip(),
77
+ "label": label,
78
+ "advice": advice,
79
+ })
80
+ return findings
81
+
82
+
83
+ def sep(char="-", width=70) -> str:
84
+ return char * width
85
+
86
+
87
+ def main() -> None:
88
+ if len(sys.argv) < 2:
89
+ print("Usage: python review.py <file.rs>")
90
+ sys.exit(1)
91
+
92
+ path = Path(sys.argv[1])
93
+ if not path.exists():
94
+ print(f"Error: file not found: {path}")
95
+ sys.exit(1)
96
+
97
+ if path.suffix.lower() != ".rs":
98
+ print(f"Warning: expected a .rs file, got '{path.suffix}' — continuing anyway")
99
+
100
+ source = path.read_text(encoding="utf-8", errors="replace")
101
+ findings = scan(source)
102
+ groups: dict[str, list] = {}
103
+ for f in findings:
104
+ groups.setdefault(f["label"], []).append(f)
105
+
106
+ print(sep("="))
107
+ print("PROGRAMMING WITH RUST — PRE-REVIEW REPORT")
108
+ print(sep("="))
109
+ print(f"File : {path}")
110
+ print(f"Lines : {len(source.splitlines())}")
111
+ print(f"Issues : {len(findings)} potential anti-patterns across {len(groups)} categories")
112
+ print()
113
+
114
+ if not findings:
115
+ print(" [OK] No common Rust anti-patterns detected.")
116
+ print()
117
+ else:
118
+ for label, items in groups.items():
119
+ print(sep())
120
+ print(f" {label} ({len(items)} occurrence{'s' if len(items) != 1 else ''})")
121
+ print(sep())
122
+ print(f" Advice: {items[0]['advice']}")
123
+ print()
124
+ for item in items[:5]:
125
+ print(f" line {item['line']:>4}: {item['text'][:100]}")
126
+ if len(items) > 5:
127
+ print(f" ... and {len(items) - 5} more")
128
+ print()
129
+
130
+ severity = (
131
+ "HIGH" if len(findings) >= 5
132
+ else "MEDIUM" if len(findings) >= 2
133
+ else "LOW" if findings
134
+ else "NONE"
135
+ )
136
+ print(sep("="))
137
+ print(f"SEVERITY: {severity} | Key chapters: Ch 8 (ownership), Ch 12 (errors), Ch 17 (traits), Ch 19 (concurrency), Ch 20 (memory)")
138
+ print(sep("="))
139
+
140
+
141
+ if __name__ == "__main__":
142
+ main()
@@ -0,0 +1,312 @@
1
+ ---
2
+ name: spring-boot-in-action
3
+ description: >
4
+ Write and review Spring Boot applications using practices from "Spring Boot in Action"
5
+ by Craig Walls. Covers auto-configuration, starter dependencies, externalizing
6
+ configuration with properties and profiles, Spring Security, testing with MockMvc
7
+ and @SpringBootTest, Spring Actuator for production observability, and deployment
8
+ strategies (JAR, WAR, Cloud Foundry). Use when building Spring Boot apps, configuring
9
+ beans, writing integration tests, setting up health checks, or deploying to production.
10
+ Trigger on: "Spring Boot", "Spring", "@SpringBootApplication", "auto-configuration",
11
+ "application.properties", "application.yml", "@RestController", "@Service",
12
+ "@Repository", "SpringBootTest", "Actuator", "starter", ".java files", "Maven", "Gradle".
13
+ ---
14
+
15
+ # Spring Boot in Action Skill
16
+
17
+ Apply the practices from Craig Walls' "Spring Boot in Action" to review existing code and write new Spring Boot applications. This skill operates in two modes: **Review Mode** (analyze code for violations of Spring Boot idioms) and **Write Mode** (produce clean, idiomatic Spring Boot from scratch).
18
+
19
+ The core philosophy: Spring Boot removes boilerplate through **auto-configuration**, **starter dependencies**, and **sensible defaults**. Fight the framework only when necessary — and when you do, prefer `application.properties` over code.
20
+
21
+ ## Reference Files
22
+
23
+ - `practices-catalog.md` — Before/after examples for auto-configuration, starters, properties, profiles, security, testing, Actuator, and deployment
24
+
25
+ ## How to Use This Skill
26
+
27
+ **Before responding**, read `practices-catalog.md` for the topic at hand. For configuration issues read the properties/profiles section. For test code read the testing section. For a full review, read all sections.
28
+
29
+ ---
30
+
31
+ ## Mode 1: Code Review
32
+
33
+ When the user asks you to **review** Spring Boot code, follow this process:
34
+
35
+ ### Step 1: Identify the Layer
36
+ Determine whether the code is a controller, service, repository, configuration class, or test. Review focus shifts by layer.
37
+
38
+ ### Step 2: Analyze the Code
39
+
40
+ Check these areas in order of severity:
41
+
42
+ 1. **Auto-Configuration** (Ch 2, 3): Is auto-configuration being fought manually? Look for `@Bean` definitions that replicate what Spring Boot already provides (DataSource, Jackson, Security, etc.). Remove manual config where auto-config suffices.
43
+
44
+ 2. **Starter Dependencies** (Ch 2): Are dependencies declared individually instead of using starters? `spring-boot-starter-web`, `spring-boot-starter-data-jpa`, `spring-boot-starter-security` etc. bundle correct transitive dependencies and version-manage them.
45
+
46
+ 3. **Externalized Configuration** (Ch 3): Are values hardcoded that belong in `application.properties`? Ports, URLs, credentials, timeouts should all be externalized. Use `@ConfigurationProperties` for type-safe config objects; use `@Value` only for single values.
47
+
48
+ 4. **Profiles** (Ch 3): Is environment-specific config (dev DB vs prod DB) handled with `if` statements or system properties? Use `@Profile` and `application-{profile}.properties` instead.
49
+
50
+ 5. **Security** (Ch 3): Is `WebSecurityConfigurerAdapter` extended when simple property-based config would suffice? Is HTTP Basic enabled in production? Are actuator endpoints exposed without auth?
51
+
52
+ 6. **Testing** (Ch 4):
53
+ - Use `@SpringBootTest` for full integration tests, not raw `new MyService()`
54
+ - Use `@WebMvcTest` for controller-only tests (no full context)
55
+ - Use `@DataJpaTest` for repository tests (in-memory DB, no web layer)
56
+ - Use `MockMvc` for controller assertions without starting a server
57
+ - Use `@MockBean` to replace real beans with mocks in slice tests
58
+ - Avoid `@SpringBootTest(webEnvironment = RANDOM_PORT)` unless testing the full HTTP stack
59
+
60
+ 7. **Actuator** (Ch 7): Is the application missing health/metrics endpoints? Is `/actuator` fully exposed without security? Are custom health indicators implemented for critical dependencies?
61
+
62
+ 8. **Deployment** (Ch 8): Is `spring.profiles.active` set for production? Is database migration (Flyway/Liquibase) configured? Is the app packaged as a self-contained JAR (preferred) or WAR?
63
+
64
+ 9. **General Idioms**:
65
+ - Constructor injection over field injection (`@Autowired` on fields)
66
+ - `@RestController` = `@Controller` + `@ResponseBody` — use it for REST APIs
67
+ - Return `ResponseEntity<T>` from controllers when status codes matter
68
+ - `Optional<T>` from repository methods, never `null`
69
+
70
+ ### Step 3: Report Findings
71
+ For each issue, report:
72
+ - **Chapter reference** (e.g., "Ch 3: Externalized Configuration")
73
+ - **Location** in the code
74
+ - **What's wrong** (the anti-pattern)
75
+ - **How to fix it** (the Spring Boot idiomatic way)
76
+ - **Priority**: Critical (security/bugs), Important (maintainability), Suggestion (polish)
77
+
78
+ ### Step 4: Provide Fixed Code
79
+ Offer a corrected version with comments explaining each change.
80
+
81
+ ---
82
+
83
+ ## Mode 2: Writing New Code
84
+
85
+ When the user asks you to **write** new Spring Boot code, apply these core principles:
86
+
87
+ ### Project Bootstrap (Ch 1, 2)
88
+
89
+ 1. **Start with Spring Initializr** (Ch 1). Use `start.spring.io` or `spring init` CLI. Select starters upfront — don't add raw dependencies manually.
90
+
91
+ 2. **Use starters, not individual dependencies** (Ch 2). `spring-boot-starter-web` includes Tomcat, Spring MVC, Jackson, and logging at compatible versions. Never declare `spring-webmvc` + `jackson-databind` + `tomcat-embed-core` separately.
92
+
93
+ 3. **The main class is the only required boilerplate** (Ch 2):
94
+ ```java
95
+ @SpringBootApplication
96
+ public class MyApp {
97
+ public static void main(String[] args) {
98
+ SpringApplication.run(MyApp.class, args);
99
+ }
100
+ }
101
+ ```
102
+ `@SpringBootApplication` = `@Configuration` + `@EnableAutoConfiguration` + `@ComponentScan`.
103
+
104
+ ### Configuration (Ch 3)
105
+
106
+ 4. **Externalize all environment-specific values** (Ch 3). Nothing deployment-specific belongs in code. Use `application.properties` / `application.yml` for defaults.
107
+
108
+ 5. **Use `@ConfigurationProperties` for grouped config** (Ch 3). Bind a prefix to a POJO — type-safe, IDE-friendly, testable:
109
+ ```java
110
+ @ConfigurationProperties(prefix = "app.mail")
111
+ @Component
112
+ public class MailProperties {
113
+ private String host;
114
+ private int port = 25;
115
+ // getters + setters
116
+ }
117
+ ```
118
+
119
+ 6. **Use profiles for environment differences** (Ch 3). `application-dev.properties` overrides `application.properties` when `spring.profiles.active=dev`. Never use `if (env.equals("production"))` in code.
120
+
121
+ 7. **Override auto-configuration surgically** (Ch 3). Use `spring.*` properties first. Only define a `@Bean` when properties are insufficient. Annotate with `@ConditionalOnMissingBean` if providing a fallback.
122
+
123
+ 8. **Customize error pages declaratively** (Ch 3). Place `error/404.html`, `error/500.html` in `src/main/resources/templates/error/`. No custom `ErrorController` needed for basic cases.
124
+
125
+ ### Security (Ch 3)
126
+
127
+ 9. **Extend `WebSecurityConfigurerAdapter` only for custom rules** (Ch 3). For simple HTTP Basic with custom users, `spring.security.user.name` / `spring.security.user.password` properties suffice.
128
+
129
+ 10. **Always secure Actuator endpoints in production** (Ch 7). Expose only `health` and `info` publicly; require authentication for `env`, `beans`, `mappings`, `shutdown`.
130
+
131
+ ### REST Controllers (Ch 2)
132
+
133
+ 11. **Use `@RestController` for API endpoints** (Ch 2). Eliminates `@ResponseBody` on every method.
134
+
135
+ 12. **Return `ResponseEntity<T>` when HTTP status matters** (Ch 2). `ResponseEntity.ok(body)`, `ResponseEntity.notFound().build()`, `ResponseEntity.status(201).body(created)`.
136
+
137
+ 13. **Use constructor injection, not field injection** (Ch 2). Constructor injection makes dependencies explicit and enables testing without Spring context:
138
+ ```java
139
+ // Prefer this:
140
+ @RestController
141
+ public class BookController {
142
+ private final BookRepository repo;
143
+ public BookController(BookRepository repo) { this.repo = repo; }
144
+ }
145
+ ```
146
+
147
+ 14. **Use `Optional` from repository queries** (Ch 2). `repo.findById(id).orElseThrow(() -> new ResponseStatusException(NOT_FOUND))`.
148
+
149
+ ### Testing (Ch 4)
150
+
151
+ 15. **Match test slice to the layer being tested** (Ch 4):
152
+ - Web layer only → `@WebMvcTest(MyController.class)` + `MockMvc`
153
+ - Repository only → `@DataJpaTest`
154
+ - Full app → `@SpringBootTest`
155
+ - External service → `@MockBean` to replace
156
+
157
+ 16. **Use `MockMvc` for controller assertions without starting a server** (Ch 4):
158
+ ```java
159
+ mockMvc.perform(get("/books/1"))
160
+ .andExpect(status().isOk())
161
+ .andExpect(jsonPath("$.title").value("Spring Boot in Action"));
162
+ ```
163
+
164
+ 17. **Use `@MockBean` to isolate the unit under test** (Ch 4). Replaces the real bean in the Spring context with a Mockito mock — cleaner than manual wiring.
165
+
166
+ 18. **Test security explicitly** (Ch 4). Use `.with(user("admin").roles("ADMIN"))` or `@WithMockUser` to assert secured endpoints reject unauthenticated requests.
167
+
168
+ ### Actuator (Ch 7)
169
+
170
+ 19. **Enable Actuator in every production app** (Ch 7). Add `spring-boot-starter-actuator`. At minimum expose `health` and `info`.
171
+
172
+ 20. **Write custom `HealthIndicator` for critical dependencies** (Ch 7):
173
+ ```java
174
+ @Component
175
+ public class DatabaseHealthIndicator implements HealthIndicator {
176
+ @Override
177
+ public Health health() {
178
+ return canConnect() ? Health.up().build()
179
+ : Health.down().withDetail("reason", "timeout").build();
180
+ }
181
+ }
182
+ ```
183
+
184
+ 21. **Add custom metrics via `MeterRegistry`** (Ch 7). Counter, gauge, timer — gives Prometheus/Grafana visibility into business events.
185
+
186
+ 22. **Restrict Actuator exposure in production** (Ch 7):
187
+ ```properties
188
+ management.endpoints.web.exposure.include=health,info
189
+ management.endpoint.health.show-details=when-authorized
190
+ ```
191
+
192
+ ### Deployment (Ch 8)
193
+
194
+ 23. **Package as an executable JAR by default** (Ch 8). `mvn package` produces a fat JAR with embedded Tomcat. Run with `java -jar app.jar`. No application server needed.
195
+
196
+ 24. **Create a production profile** (Ch 8). `application-production.properties` sets `spring.datasource.url`, disables dev tools, sets log levels to WARN.
197
+
198
+ 25. **Use Flyway or Liquibase for database migrations** (Ch 8). Add `spring-boot-starter-flyway`; place scripts in `classpath:db/migration/V1__init.sql`. Never use `spring.jpa.hibernate.ddl-auto=create` in production.
199
+
200
+ ---
201
+
202
+ ## Starter Cheat Sheet (Ch 2, Appendix B)
203
+
204
+ | Need | Starter |
205
+ |------|---------|
206
+ | REST API | `spring-boot-starter-web` |
207
+ | JPA / Hibernate | `spring-boot-starter-data-jpa` |
208
+ | Security | `spring-boot-starter-security` |
209
+ | Observability | `spring-boot-starter-actuator` |
210
+ | Testing | `spring-boot-starter-test` |
211
+ | Thymeleaf views | `spring-boot-starter-thymeleaf` |
212
+ | Redis cache | `spring-boot-starter-data-redis` |
213
+ | Messaging | `spring-boot-starter-amqp` |
214
+ | DB migration | `flyway-core` |
215
+
216
+ ---
217
+
218
+ ## Code Structure Template
219
+
220
+ ```java
221
+ // Main class (Ch 2)
222
+ @SpringBootApplication
223
+ public class LibraryApp {
224
+ public static void main(String[] args) {
225
+ SpringApplication.run(LibraryApp.class, args);
226
+ }
227
+ }
228
+
229
+ // Entity (Ch 2)
230
+ @Entity
231
+ public class Book {
232
+ @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
233
+ private Long id;
234
+ private String title;
235
+ private String isbn;
236
+ // constructors, getters, setters
237
+ }
238
+
239
+ // Repository (Ch 2)
240
+ public interface BookRepository extends JpaRepository<Book, Long> {
241
+ List<Book> findByTitleContainingIgnoreCase(String title);
242
+ }
243
+
244
+ // Service (Ch 2) — constructor injection
245
+ @Service
246
+ public class BookService {
247
+ private final BookRepository repo;
248
+ public BookService(BookRepository repo) { this.repo = repo; }
249
+
250
+ public Book findById(Long id) {
251
+ return repo.findById(id)
252
+ .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
253
+ }
254
+ }
255
+
256
+ // Controller (Ch 2)
257
+ @RestController
258
+ @RequestMapping("/api/books")
259
+ public class BookController {
260
+ private final BookService service;
261
+ public BookController(BookService service) { this.service = service; }
262
+
263
+ @GetMapping("/{id}")
264
+ public ResponseEntity<Book> getBook(@PathVariable Long id) {
265
+ return ResponseEntity.ok(service.findById(id));
266
+ }
267
+
268
+ @PostMapping
269
+ public ResponseEntity<Book> createBook(@RequestBody Book book) {
270
+ Book saved = service.save(book);
271
+ URI location = URI.create("/api/books/" + saved.getId());
272
+ return ResponseEntity.created(location).body(saved);
273
+ }
274
+ }
275
+
276
+ // application.properties (Ch 3)
277
+ // spring.datasource.url=jdbc:postgresql://localhost/library
278
+ // spring.datasource.username=${DB_USER}
279
+ // spring.datasource.password=${DB_PASS}
280
+ // spring.jpa.hibernate.ddl-auto=validate
281
+ // management.endpoints.web.exposure.include=health,info
282
+
283
+ // application-dev.properties (Ch 3)
284
+ // spring.datasource.url=jdbc:h2:mem:library
285
+ // spring.jpa.hibernate.ddl-auto=create-drop
286
+ // logging.level.org.springframework=DEBUG
287
+ ```
288
+
289
+ ---
290
+
291
+ ## Priority of Practices by Impact
292
+
293
+ ### Critical (Security & Correctness)
294
+ - Ch 3: Never hardcode credentials — use `${ENV_VAR}` in properties
295
+ - Ch 3: Secure Actuator endpoints — `env`, `beans`, `shutdown` must require auth
296
+ - Ch 4: Test secured endpoints explicitly — assert 401/403 on unauthenticated requests
297
+ - Ch 8: Never use `ddl-auto=create` in production — use Flyway/Liquibase
298
+
299
+ ### Important (Idiom & Maintainability)
300
+ - Ch 2: Constructor injection over `@Autowired` field injection
301
+ - Ch 2: `@RestController` over `@Controller` + `@ResponseBody` for APIs
302
+ - Ch 2: `Optional` from repository, never `null`
303
+ - Ch 3: `@ConfigurationProperties` over scattered `@Value` for grouped config
304
+ - Ch 3: Profiles for environment differences — not `if` statements
305
+ - Ch 4: `@WebMvcTest` for controller tests — not full `@SpringBootTest`
306
+ - Ch 7: Custom `HealthIndicator` for each critical dependency
307
+
308
+ ### Suggestions (Polish)
309
+ - Ch 3: Custom error pages in `templates/error/` — no code needed
310
+ - Ch 7: Custom metrics via `MeterRegistry` for business events
311
+ - Ch 8: Production profile disables dev tools, sets WARN log level
312
+ - Ch 2: Use `spring-boot-devtools` in dev for live reload