mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-03-30 13:43:26 +08:00
docs: expand Spring Boot skills and add Go microservice example
- springboot-security: add code examples for authorization, input validation, SQL injection prevention, password encoding, CORS, rate limiting, and secrets management (119 → 261 lines) - springboot-verification: add unit test, Testcontainers integration test, MockMvc API test patterns, and security scan grep commands (100 → 222 lines) - Add Go microservice example (gRPC + PostgreSQL + clean architecture) - Update README directory tree with new example
This commit is contained in:
@@ -318,7 +318,8 @@ everything-claude-code/
|
||||
|-- examples/ # Example configurations and sessions
|
||||
| |-- CLAUDE.md # Example project-level config
|
||||
| |-- user-CLAUDE.md # Example user-level config
|
||||
| |-- saas-nextjs-CLAUDE.md # Real-world SaaS (Next.js + Supabase + Stripe)
|
||||
| |-- saas-nextjs-CLAUDE.md # Real-world SaaS (Next.js + Supabase + Stripe)
|
||||
| |-- go-microservice-CLAUDE.md # Real-world Go microservice (gRPC + PostgreSQL)
|
||||
|
|
||||
|-- mcp-configs/ # MCP server configurations
|
||||
| |-- mcp-servers.json # GitHub, Supabase, Vercel, Railway, etc.
|
||||
|
||||
267
examples/go-microservice-CLAUDE.md
Normal file
267
examples/go-microservice-CLAUDE.md
Normal file
@@ -0,0 +1,267 @@
|
||||
# Go Microservice — Project CLAUDE.md
|
||||
|
||||
> Real-world example for a Go microservice with PostgreSQL, gRPC, and Docker.
|
||||
> Copy this to your project root and customize for your service.
|
||||
|
||||
## Project Overview
|
||||
|
||||
**Stack:** Go 1.22+, PostgreSQL, gRPC + REST (grpc-gateway), Docker, sqlc (type-safe SQL), Wire (dependency injection)
|
||||
|
||||
**Architecture:** Clean architecture with domain, repository, service, and handler layers. gRPC as primary transport with REST gateway for external clients.
|
||||
|
||||
## Critical Rules
|
||||
|
||||
### Go Conventions
|
||||
|
||||
- Follow Effective Go and the Go Code Review Comments guide
|
||||
- Use `errors.New` / `fmt.Errorf` with `%w` for wrapping — never string matching on errors
|
||||
- No `init()` functions — explicit initialization in `main()` or constructors
|
||||
- No global mutable state — pass dependencies via constructors
|
||||
- Context must be the first parameter and propagated through all layers
|
||||
|
||||
### Database
|
||||
|
||||
- All queries in `queries/` as plain SQL — sqlc generates type-safe Go code
|
||||
- Migrations in `migrations/` using golang-migrate — never alter the database directly
|
||||
- Use transactions for multi-step operations via `pgx.Tx`
|
||||
- All queries must use parameterized placeholders (`$1`, `$2`) — never string formatting
|
||||
|
||||
### Error Handling
|
||||
|
||||
- Return errors, don't panic — panics are only for truly unrecoverable situations
|
||||
- Wrap errors with context: `fmt.Errorf("creating user: %w", err)`
|
||||
- Define sentinel errors in `domain/errors.go` for business logic
|
||||
- Map domain errors to gRPC status codes in the handler layer
|
||||
|
||||
```go
|
||||
// Domain layer — sentinel errors
|
||||
var (
|
||||
ErrUserNotFound = errors.New("user not found")
|
||||
ErrEmailTaken = errors.New("email already registered")
|
||||
)
|
||||
|
||||
// Handler layer — map to gRPC status
|
||||
func toGRPCError(err error) error {
|
||||
switch {
|
||||
case errors.Is(err, domain.ErrUserNotFound):
|
||||
return status.Error(codes.NotFound, err.Error())
|
||||
case errors.Is(err, domain.ErrEmailTaken):
|
||||
return status.Error(codes.AlreadyExists, err.Error())
|
||||
default:
|
||||
return status.Error(codes.Internal, "internal error")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Code Style
|
||||
|
||||
- No emojis in code or comments
|
||||
- Exported types and functions must have doc comments
|
||||
- Keep functions under 50 lines — extract helpers
|
||||
- Use table-driven tests for all logic with multiple cases
|
||||
- Prefer `struct{}` for signal channels, not `bool`
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
cmd/
|
||||
server/
|
||||
main.go # Entrypoint, Wire injection, graceful shutdown
|
||||
internal/
|
||||
domain/ # Business types and interfaces
|
||||
user.go # User entity and repository interface
|
||||
errors.go # Sentinel errors
|
||||
service/ # Business logic
|
||||
user_service.go
|
||||
user_service_test.go
|
||||
repository/ # Data access (sqlc-generated + custom)
|
||||
postgres/
|
||||
user_repo.go
|
||||
user_repo_test.go # Integration tests with testcontainers
|
||||
handler/ # gRPC + REST handlers
|
||||
grpc/
|
||||
user_handler.go
|
||||
rest/
|
||||
user_handler.go
|
||||
config/ # Configuration loading
|
||||
config.go
|
||||
proto/ # Protobuf definitions
|
||||
user/v1/
|
||||
user.proto
|
||||
queries/ # SQL queries for sqlc
|
||||
user.sql
|
||||
migrations/ # Database migrations
|
||||
001_create_users.up.sql
|
||||
001_create_users.down.sql
|
||||
```
|
||||
|
||||
## Key Patterns
|
||||
|
||||
### Repository Interface
|
||||
|
||||
```go
|
||||
type UserRepository interface {
|
||||
Create(ctx context.Context, user *User) error
|
||||
FindByID(ctx context.Context, id uuid.UUID) (*User, error)
|
||||
FindByEmail(ctx context.Context, email string) (*User, error)
|
||||
Update(ctx context.Context, user *User) error
|
||||
Delete(ctx context.Context, id uuid.UUID) error
|
||||
}
|
||||
```
|
||||
|
||||
### Service with Dependency Injection
|
||||
|
||||
```go
|
||||
type UserService struct {
|
||||
repo domain.UserRepository
|
||||
hasher PasswordHasher
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
func NewUserService(repo domain.UserRepository, hasher PasswordHasher, logger *slog.Logger) *UserService {
|
||||
return &UserService{repo: repo, hasher: hasher, logger: logger}
|
||||
}
|
||||
|
||||
func (s *UserService) Create(ctx context.Context, req CreateUserRequest) (*domain.User, error) {
|
||||
existing, err := s.repo.FindByEmail(ctx, req.Email)
|
||||
if err != nil && !errors.Is(err, domain.ErrUserNotFound) {
|
||||
return nil, fmt.Errorf("checking email: %w", err)
|
||||
}
|
||||
if existing != nil {
|
||||
return nil, domain.ErrEmailTaken
|
||||
}
|
||||
|
||||
hashed, err := s.hasher.Hash(req.Password)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("hashing password: %w", err)
|
||||
}
|
||||
|
||||
user := &domain.User{
|
||||
ID: uuid.New(),
|
||||
Name: req.Name,
|
||||
Email: req.Email,
|
||||
Password: hashed,
|
||||
}
|
||||
if err := s.repo.Create(ctx, user); err != nil {
|
||||
return nil, fmt.Errorf("creating user: %w", err)
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
```
|
||||
|
||||
### Table-Driven Tests
|
||||
|
||||
```go
|
||||
func TestUserService_Create(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
req CreateUserRequest
|
||||
setup func(*MockUserRepo)
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "valid user",
|
||||
req: CreateUserRequest{Name: "Alice", Email: "alice@example.com", Password: "secure123"},
|
||||
setup: func(m *MockUserRepo) {
|
||||
m.On("FindByEmail", mock.Anything, "alice@example.com").Return(nil, domain.ErrUserNotFound)
|
||||
m.On("Create", mock.Anything, mock.Anything).Return(nil)
|
||||
},
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "duplicate email",
|
||||
req: CreateUserRequest{Name: "Alice", Email: "taken@example.com", Password: "secure123"},
|
||||
setup: func(m *MockUserRepo) {
|
||||
m.On("FindByEmail", mock.Anything, "taken@example.com").Return(&domain.User{}, nil)
|
||||
},
|
||||
wantErr: domain.ErrEmailTaken,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
repo := new(MockUserRepo)
|
||||
tt.setup(repo)
|
||||
svc := NewUserService(repo, &bcryptHasher{}, slog.Default())
|
||||
|
||||
_, err := svc.Create(context.Background(), tt.req)
|
||||
|
||||
if tt.wantErr != nil {
|
||||
assert.ErrorIs(t, err, tt.wantErr)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
```bash
|
||||
# Database
|
||||
DATABASE_URL=postgres://user:pass@localhost:5432/myservice?sslmode=disable
|
||||
|
||||
# gRPC
|
||||
GRPC_PORT=50051
|
||||
REST_PORT=8080
|
||||
|
||||
# Auth
|
||||
JWT_SECRET= # Load from vault in production
|
||||
TOKEN_EXPIRY=24h
|
||||
|
||||
# Observability
|
||||
LOG_LEVEL=info # debug, info, warn, error
|
||||
OTEL_ENDPOINT= # OpenTelemetry collector
|
||||
```
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
```bash
|
||||
/go-test # TDD workflow for Go
|
||||
/go-review # Go-specific code review
|
||||
/go-build # Fix build errors
|
||||
```
|
||||
|
||||
### Test Commands
|
||||
|
||||
```bash
|
||||
# Unit tests (fast, no external deps)
|
||||
go test ./internal/... -short -count=1
|
||||
|
||||
# Integration tests (requires Docker for testcontainers)
|
||||
go test ./internal/repository/... -count=1 -timeout 120s
|
||||
|
||||
# All tests with coverage
|
||||
go test ./... -coverprofile=coverage.out -count=1
|
||||
go tool cover -func=coverage.out # summary
|
||||
go tool cover -html=coverage.out # browser
|
||||
|
||||
# Race detector
|
||||
go test ./... -race -count=1
|
||||
```
|
||||
|
||||
## ECC Workflow
|
||||
|
||||
```bash
|
||||
# Planning
|
||||
/plan "Add rate limiting to user endpoints"
|
||||
|
||||
# Development
|
||||
/go-test # TDD with Go-specific patterns
|
||||
|
||||
# Review
|
||||
/go-review # Go idioms, error handling, concurrency
|
||||
/security-scan # Secrets and vulnerabilities
|
||||
|
||||
# Before merge
|
||||
go vet ./...
|
||||
staticcheck ./...
|
||||
```
|
||||
|
||||
## Git Workflow
|
||||
|
||||
- `feat:` new features, `fix:` bug fixes, `refactor:` code changes
|
||||
- Feature branches from `main`, PRs required
|
||||
- CI: `go vet`, `staticcheck`, `go test -race`, `golangci-lint`
|
||||
- Deploy: Docker image built in CI, deployed to Kubernetes
|
||||
@@ -42,17 +42,88 @@ public class JwtAuthFilter extends OncePerRequestFilter {
|
||||
- Use `@PreAuthorize("hasRole('ADMIN')")` or `@PreAuthorize("@authz.canEdit(#id)")`
|
||||
- Deny by default; expose only required scopes
|
||||
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/api/admin")
|
||||
public class AdminController {
|
||||
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
@GetMapping("/users")
|
||||
public List<UserDto> listUsers() {
|
||||
return userService.findAll();
|
||||
}
|
||||
|
||||
@PreAuthorize("@authz.isOwner(#id, authentication)")
|
||||
@DeleteMapping("/users/{id}")
|
||||
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
|
||||
userService.delete(id);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Input Validation
|
||||
|
||||
- Use Bean Validation with `@Valid` on controllers
|
||||
- Apply constraints on DTOs: `@NotBlank`, `@Email`, `@Size`, custom validators
|
||||
- Sanitize any HTML with a whitelist before rendering
|
||||
|
||||
```java
|
||||
// BAD: No validation
|
||||
@PostMapping("/users")
|
||||
public User createUser(@RequestBody UserDto dto) {
|
||||
return userService.create(dto);
|
||||
}
|
||||
|
||||
// GOOD: Validated DTO
|
||||
public record CreateUserDto(
|
||||
@NotBlank @Size(max = 100) String name,
|
||||
@NotBlank @Email String email,
|
||||
@NotNull @Min(0) @Max(150) Integer age
|
||||
) {}
|
||||
|
||||
@PostMapping("/users")
|
||||
public ResponseEntity<UserDto> createUser(@Valid @RequestBody CreateUserDto dto) {
|
||||
return ResponseEntity.status(HttpStatus.CREATED)
|
||||
.body(userService.create(dto));
|
||||
}
|
||||
```
|
||||
|
||||
## SQL Injection Prevention
|
||||
|
||||
- Use Spring Data repositories or parameterized queries
|
||||
- For native queries, use `:param` bindings; never concatenate strings
|
||||
|
||||
```java
|
||||
// BAD: String concatenation in native query
|
||||
@Query(value = "SELECT * FROM users WHERE name = '" + name + "'", nativeQuery = true)
|
||||
|
||||
// GOOD: Parameterized native query
|
||||
@Query(value = "SELECT * FROM users WHERE name = :name", nativeQuery = true)
|
||||
List<User> findByName(@Param("name") String name);
|
||||
|
||||
// GOOD: Spring Data derived query (auto-parameterized)
|
||||
List<User> findByEmailAndActiveTrue(String email);
|
||||
```
|
||||
|
||||
## Password Encoding
|
||||
|
||||
- Always hash passwords with BCrypt or Argon2 — never store plaintext
|
||||
- Use `PasswordEncoder` bean, not manual hashing
|
||||
|
||||
```java
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder(12); // cost factor 12
|
||||
}
|
||||
|
||||
// In service
|
||||
public User register(CreateUserDto dto) {
|
||||
String hashedPassword = passwordEncoder.encode(dto.password());
|
||||
return userRepository.save(new User(dto.email(), hashedPassword));
|
||||
}
|
||||
```
|
||||
|
||||
## CSRF Protection
|
||||
|
||||
- For browser session apps, keep CSRF enabled; include token in forms/headers
|
||||
@@ -70,6 +141,25 @@ http
|
||||
- Keep `application.yml` free of credentials; use placeholders
|
||||
- Rotate tokens and DB credentials regularly
|
||||
|
||||
```yaml
|
||||
# BAD: Hardcoded in application.yml
|
||||
spring:
|
||||
datasource:
|
||||
password: mySecretPassword123
|
||||
|
||||
# GOOD: Environment variable placeholder
|
||||
spring:
|
||||
datasource:
|
||||
password: ${DB_PASSWORD}
|
||||
|
||||
# GOOD: Spring Cloud Vault integration
|
||||
spring:
|
||||
cloud:
|
||||
vault:
|
||||
uri: https://vault.example.com
|
||||
token: ${VAULT_TOKEN}
|
||||
```
|
||||
|
||||
## Security Headers
|
||||
|
||||
```java
|
||||
@@ -82,11 +172,63 @@ http
|
||||
.referrerPolicy(rp -> rp.policy(ReferrerPolicyHeaderWriter.ReferrerPolicy.NO_REFERRER)));
|
||||
```
|
||||
|
||||
## CORS Configuration
|
||||
|
||||
- Configure CORS at the security filter level, not per-controller
|
||||
- Restrict allowed origins — never use `*` in production
|
||||
|
||||
```java
|
||||
@Bean
|
||||
public CorsConfigurationSource corsConfigurationSource() {
|
||||
CorsConfiguration config = new CorsConfiguration();
|
||||
config.setAllowedOrigins(List.of("https://app.example.com"));
|
||||
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
|
||||
config.setAllowedHeaders(List.of("Authorization", "Content-Type"));
|
||||
config.setAllowCredentials(true);
|
||||
config.setMaxAge(3600L);
|
||||
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/api/**", config);
|
||||
return source;
|
||||
}
|
||||
|
||||
// In SecurityFilterChain:
|
||||
http.cors(cors -> cors.configurationSource(corsConfigurationSource()));
|
||||
```
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
- Apply Bucket4j or gateway-level limits on expensive endpoints
|
||||
- Log and alert on bursts; return 429 with retry hints
|
||||
|
||||
```java
|
||||
// Using Bucket4j for per-endpoint rate limiting
|
||||
@Component
|
||||
public class RateLimitFilter extends OncePerRequestFilter {
|
||||
private final Map<String, Bucket> buckets = new ConcurrentHashMap<>();
|
||||
|
||||
private Bucket createBucket() {
|
||||
return Bucket.builder()
|
||||
.addLimit(Bandwidth.classic(100, Refill.intervally(100, Duration.ofMinutes(1))))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
|
||||
FilterChain chain) throws ServletException, IOException {
|
||||
String clientIp = request.getRemoteAddr();
|
||||
Bucket bucket = buckets.computeIfAbsent(clientIp, k -> createBucket());
|
||||
|
||||
if (bucket.tryConsume(1)) {
|
||||
chain.doFilter(request, response);
|
||||
} else {
|
||||
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
|
||||
response.getWriter().write("{\"error\": \"Rate limit exceeded\"}");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Dependency Security
|
||||
|
||||
- Run OWASP Dependency Check / Snyk in CI
|
||||
|
||||
@@ -42,6 +42,111 @@ Report:
|
||||
- Total tests, passed/failed
|
||||
- Coverage % (lines/branches)
|
||||
|
||||
### Unit Tests
|
||||
|
||||
Test service logic in isolation with mocked dependencies:
|
||||
|
||||
```java
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class UserServiceTest {
|
||||
|
||||
@Mock private UserRepository userRepository;
|
||||
@InjectMocks private UserService userService;
|
||||
|
||||
@Test
|
||||
void createUser_validInput_returnsUser() {
|
||||
var dto = new CreateUserDto("Alice", "alice@example.com");
|
||||
var expected = new User(1L, "Alice", "alice@example.com");
|
||||
when(userRepository.save(any(User.class))).thenReturn(expected);
|
||||
|
||||
var result = userService.create(dto);
|
||||
|
||||
assertThat(result.name()).isEqualTo("Alice");
|
||||
verify(userRepository).save(any(User.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void createUser_duplicateEmail_throwsException() {
|
||||
var dto = new CreateUserDto("Alice", "existing@example.com");
|
||||
when(userRepository.existsByEmail(dto.email())).thenReturn(true);
|
||||
|
||||
assertThatThrownBy(() -> userService.create(dto))
|
||||
.isInstanceOf(DuplicateEmailException.class);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Integration Tests with Testcontainers
|
||||
|
||||
Test against a real database instead of H2:
|
||||
|
||||
```java
|
||||
@SpringBootTest
|
||||
@Testcontainers
|
||||
class UserRepositoryIntegrationTest {
|
||||
|
||||
@Container
|
||||
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine")
|
||||
.withDatabaseName("testdb");
|
||||
|
||||
@DynamicPropertySource
|
||||
static void configureProperties(DynamicPropertyRegistry registry) {
|
||||
registry.add("spring.datasource.url", postgres::getJdbcUrl);
|
||||
registry.add("spring.datasource.username", postgres::getUsername);
|
||||
registry.add("spring.datasource.password", postgres::getPassword);
|
||||
}
|
||||
|
||||
@Autowired private UserRepository userRepository;
|
||||
|
||||
@Test
|
||||
void findByEmail_existingUser_returnsUser() {
|
||||
userRepository.save(new User("Alice", "alice@example.com"));
|
||||
|
||||
var found = userRepository.findByEmail("alice@example.com");
|
||||
|
||||
assertThat(found).isPresent();
|
||||
assertThat(found.get().getName()).isEqualTo("Alice");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### API Tests with MockMvc
|
||||
|
||||
Test controller layer with full Spring context:
|
||||
|
||||
```java
|
||||
@WebMvcTest(UserController.class)
|
||||
class UserControllerTest {
|
||||
|
||||
@Autowired private MockMvc mockMvc;
|
||||
@MockBean private UserService userService;
|
||||
|
||||
@Test
|
||||
void createUser_validInput_returns201() throws Exception {
|
||||
var user = new UserDto(1L, "Alice", "alice@example.com");
|
||||
when(userService.create(any())).thenReturn(user);
|
||||
|
||||
mockMvc.perform(post("/api/users")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{"name": "Alice", "email": "alice@example.com"}
|
||||
"""))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.name").value("Alice"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void createUser_invalidEmail_returns400() throws Exception {
|
||||
mockMvc.perform(post("/api/users")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{"name": "Alice", "email": "not-an-email"}
|
||||
"""))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Phase 4: Security Scan
|
||||
|
||||
```bash
|
||||
@@ -50,10 +155,27 @@ mvn org.owasp:dependency-check-maven:check
|
||||
# or
|
||||
./gradlew dependencyCheckAnalyze
|
||||
|
||||
# Secrets (git)
|
||||
# Secrets in source
|
||||
grep -rn "password\s*=\s*\"" src/ --include="*.java" --include="*.yml" --include="*.properties"
|
||||
grep -rn "sk-\|api_key\|secret" src/ --include="*.java" --include="*.yml"
|
||||
|
||||
# Secrets (git history)
|
||||
git secrets --scan # if configured
|
||||
```
|
||||
|
||||
### Common Security Findings
|
||||
|
||||
```
|
||||
# Check for System.out.println (use logger instead)
|
||||
grep -rn "System\.out\.print" src/main/ --include="*.java"
|
||||
|
||||
# Check for raw exception messages in responses
|
||||
grep -rn "e\.getMessage()" src/main/ --include="*.java"
|
||||
|
||||
# Check for wildcard CORS
|
||||
grep -rn "allowedOrigins.*\*" src/main/ --include="*.java"
|
||||
```
|
||||
|
||||
## Phase 5: Lint/Format (optional gate)
|
||||
|
||||
```bash
|
||||
|
||||
Reference in New Issue
Block a user