mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-03-30 13:43:26 +08:00
- 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
268 lines
7.6 KiB
Markdown
268 lines
7.6 KiB
Markdown
# 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
|