feat: initial Claude Code configuration scaffold

Comprehensive Claude Code guidance system with:

- 5 agents: tdd-guardian, code-reviewer, security-scanner, refactor-scan, dependency-audit
- 18 skills covering languages (Python, TypeScript, Rust, Go, Java, C#),
  infrastructure (AWS, Azure, GCP, Terraform, Ansible, Docker/K8s, Database, CI/CD),
  testing (TDD, UI, Browser), and patterns (Monorepo, API Design, Observability)
- 3 hooks: secret detection, auto-formatting, TDD git pre-commit
- Strict TDD enforcement with 80%+ coverage requirements
- Multi-model strategy: Opus for planning, Sonnet for execution (opusplan)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-20 15:47:34 -05:00
commit befb8fbaeb
34 changed files with 12233 additions and 0 deletions

View File

@@ -0,0 +1,666 @@
---
name: csharp-development
description: C# and .NET development patterns with xUnit, Clean Architecture, and modern C# practices. Use when writing C# code.
---
# C# Development Skill
## Project Structure (Clean Architecture)
```
Solution/
├── src/
│ ├── Domain/ # Enterprise business rules
│ │ ├── Entities/
│ │ ├── ValueObjects/
│ │ ├── Exceptions/
│ │ └── Domain.csproj
│ ├── Application/ # Application business rules
│ │ ├── Common/
│ │ │ ├── Interfaces/
│ │ │ └── Behaviours/
│ │ ├── Features/
│ │ │ └── Users/
│ │ │ ├── Commands/
│ │ │ └── Queries/
│ │ └── Application.csproj
│ ├── Infrastructure/ # External concerns
│ │ ├── Persistence/
│ │ ├── Identity/
│ │ └── Infrastructure.csproj
│ └── Api/ # Presentation layer
│ ├── Controllers/
│ ├── Middleware/
│ └── Api.csproj
├── tests/
│ ├── Domain.Tests/
│ ├── Application.Tests/
│ ├── Infrastructure.Tests/
│ └── Api.Tests/
└── Solution.sln
```
## Testing with xUnit
### Unit Tests with Fluent Assertions
```csharp
using FluentAssertions;
using Moq;
using Xunit;
namespace Application.Tests.Features.Users;
public class CreateUserCommandHandlerTests
{
private readonly Mock<IUserRepository> _userRepositoryMock;
private readonly Mock<IUnitOfWork> _unitOfWorkMock;
private readonly CreateUserCommandHandler _handler;
public CreateUserCommandHandlerTests()
{
_userRepositoryMock = new Mock<IUserRepository>();
_unitOfWorkMock = new Mock<IUnitOfWork>();
_handler = new CreateUserCommandHandler(
_userRepositoryMock.Object,
_unitOfWorkMock.Object);
}
[Fact]
public async Task Handle_WithValidCommand_ShouldCreateUser()
{
// Arrange
var command = UserFixtures.CreateUserCommand();
_userRepositoryMock
.Setup(x => x.ExistsByEmailAsync(command.Email, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
// Act
var result = await _handler.Handle(command, CancellationToken.None);
// Assert
result.Should().NotBeNull();
result.Id.Should().NotBeEmpty();
result.Email.Should().Be(command.Email);
_userRepositoryMock.Verify(
x => x.AddAsync(It.IsAny<User>(), It.IsAny<CancellationToken>()),
Times.Once);
_unitOfWorkMock.Verify(
x => x.SaveChangesAsync(It.IsAny<CancellationToken>()),
Times.Once);
}
[Fact]
public async Task Handle_WithDuplicateEmail_ShouldThrowDuplicateEmailException()
{
// Arrange
var command = UserFixtures.CreateUserCommand();
_userRepositoryMock
.Setup(x => x.ExistsByEmailAsync(command.Email, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
// Act
var act = () => _handler.Handle(command, CancellationToken.None);
// Assert
await act.Should()
.ThrowAsync<DuplicateEmailException>()
.WithMessage($"*{command.Email}*");
}
}
```
### Test Fixtures
```csharp
namespace Application.Tests.Fixtures;
public static class UserFixtures
{
public static User User(Action<User>? customize = null)
{
var user = new User
{
Id = Guid.NewGuid(),
Email = "test@example.com",
Name = "Test User",
Role = Role.User,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
};
customize?.Invoke(user);
return user;
}
public static CreateUserCommand CreateUserCommand(
Action<CreateUserCommand>? customize = null)
{
var command = new CreateUserCommand
{
Email = "newuser@example.com",
Name = "New User",
Password = "SecurePass123!"
};
customize?.Invoke(command);
return command;
}
public static UserDto UserDto(Action<UserDto>? customize = null)
{
var dto = new UserDto
{
Id = Guid.NewGuid(),
Email = "test@example.com",
Name = "Test User",
Role = Role.User.ToString()
};
customize?.Invoke(dto);
return dto;
}
}
// Usage
var adminUser = UserFixtures.User(u => u.Role = Role.Admin);
var customCommand = UserFixtures.CreateUserCommand(c => c.Email = "custom@example.com");
```
### Theory Tests (Parameterized)
```csharp
namespace Domain.Tests.ValueObjects;
public class EmailTests
{
[Theory]
[InlineData("user@example.com", true)]
[InlineData("user.name@example.co.uk", true)]
[InlineData("invalid", false)]
[InlineData("missing@domain", false)]
[InlineData("", false)]
[InlineData(null, false)]
public void IsValid_ShouldReturnExpectedResult(string? email, bool expected)
{
// Act
var result = Email.IsValid(email);
// Assert
result.Should().Be(expected);
}
[Theory]
[MemberData(nameof(ValidEmailTestData))]
public void Create_WithValidEmail_ShouldSucceed(string email)
{
// Act
var result = Email.Create(email);
// Assert
result.IsSuccess.Should().BeTrue();
result.Value.Value.Should().Be(email.ToLowerInvariant());
}
public static IEnumerable<object[]> ValidEmailTestData()
{
yield return new object[] { "user@example.com" };
yield return new object[] { "USER@EXAMPLE.COM" };
yield return new object[] { "user.name+tag@example.co.uk" };
}
}
```
### Integration Tests with WebApplicationFactory
```csharp
using Microsoft.AspNetCore.Mvc.Testing;
using System.Net;
using System.Net.Http.Json;
namespace Api.Tests.Controllers;
public class UsersControllerTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly HttpClient _client;
private readonly WebApplicationFactory<Program> _factory;
public UsersControllerTests(WebApplicationFactory<Program> factory)
{
_factory = factory.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
// Replace services with test doubles
services.RemoveAll<IUserRepository>();
services.AddScoped<IUserRepository, InMemoryUserRepository>();
});
});
_client = _factory.CreateClient();
}
[Fact]
public async Task CreateUser_WithValidRequest_ReturnsCreated()
{
// Arrange
var request = new CreateUserRequest
{
Email = "new@example.com",
Name = "New User",
Password = "SecurePass123!"
};
// Act
var response = await _client.PostAsJsonAsync("/api/users", request);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.Created);
var user = await response.Content.ReadFromJsonAsync<UserDto>();
user.Should().NotBeNull();
user!.Email.Should().Be(request.Email);
}
[Fact]
public async Task GetUser_WhenNotFound_ReturnsNotFound()
{
// Act
var response = await _client.GetAsync($"/api/users/{Guid.NewGuid()}");
// Assert
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
}
}
```
## Domain Models
### Entity with Value Objects
```csharp
namespace Domain.Entities;
public class User : BaseEntity
{
private User() { } // EF Core constructor
public Guid Id { get; private set; }
public Email Email { get; private set; } = null!;
public string Name { get; private set; } = null!;
public Role Role { get; private set; }
public DateTime CreatedAt { get; private set; }
public DateTime UpdatedAt { get; private set; }
public static Result<User> Create(string email, string name)
{
var emailResult = Email.Create(email);
if (emailResult.IsFailure)
{
return Result.Failure<User>(emailResult.Error);
}
if (string.IsNullOrWhiteSpace(name))
{
return Result.Failure<User>(DomainErrors.User.NameRequired);
}
var user = new User
{
Id = Guid.NewGuid(),
Email = emailResult.Value,
Name = name.Trim(),
Role = Role.User,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
};
user.RaiseDomainEvent(new UserCreatedEvent(user.Id, user.Email.Value));
return Result.Success(user);
}
public Result UpdateName(string name)
{
if (string.IsNullOrWhiteSpace(name))
{
return Result.Failure(DomainErrors.User.NameRequired);
}
Name = name.Trim();
UpdatedAt = DateTime.UtcNow;
return Result.Success();
}
public void PromoteToAdmin()
{
Role = Role.Admin;
UpdatedAt = DateTime.UtcNow;
RaiseDomainEvent(new UserPromotedEvent(Id));
}
}
```
### Value Object
```csharp
namespace Domain.ValueObjects;
public sealed class Email : ValueObject
{
private static readonly Regex EmailRegex = new(
@"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
RegexOptions.Compiled);
public string Value { get; }
private Email(string value) => Value = value;
public static Result<Email> Create(string? email)
{
if (string.IsNullOrWhiteSpace(email))
{
return Result.Failure<Email>(DomainErrors.Email.Empty);
}
var normalizedEmail = email.Trim().ToLowerInvariant();
if (!EmailRegex.IsMatch(normalizedEmail))
{
return Result.Failure<Email>(DomainErrors.Email.InvalidFormat);
}
return Result.Success(new Email(normalizedEmail));
}
public static bool IsValid(string? email) =>
!string.IsNullOrWhiteSpace(email) && EmailRegex.IsMatch(email);
protected override IEnumerable<object> GetEqualityComponents()
{
yield return Value;
}
public override string ToString() => Value;
public static implicit operator string(Email email) => email.Value;
}
```
### Result Pattern
```csharp
namespace Domain.Common;
public class Result
{
protected Result(bool isSuccess, Error error)
{
if (isSuccess && error != Error.None ||
!isSuccess && error == Error.None)
{
throw new ArgumentException("Invalid error", nameof(error));
}
IsSuccess = isSuccess;
Error = error;
}
public bool IsSuccess { get; }
public bool IsFailure => !IsSuccess;
public Error Error { get; }
public static Result Success() => new(true, Error.None);
public static Result Failure(Error error) => new(false, error);
public static Result<T> Success<T>(T value) => new(value, true, Error.None);
public static Result<T> Failure<T>(Error error) => new(default!, false, error);
}
public class Result<T> : Result
{
private readonly T _value;
protected internal Result(T value, bool isSuccess, Error error)
: base(isSuccess, error)
{
_value = value;
}
public T Value => IsSuccess
? _value
: throw new InvalidOperationException("Cannot access value of failed result");
}
public record Error(string Code, string Message)
{
public static readonly Error None = new(string.Empty, string.Empty);
}
```
## CQRS with MediatR
### Command
```csharp
namespace Application.Features.Users.Commands;
public record CreateUserCommand : IRequest<Result<UserDto>>
{
public required string Email { get; init; }
public required string Name { get; init; }
public required string Password { get; init; }
}
public class CreateUserCommandValidator : AbstractValidator<CreateUserCommand>
{
public CreateUserCommandValidator()
{
RuleFor(x => x.Email)
.NotEmpty()
.EmailAddress()
.MaximumLength(255);
RuleFor(x => x.Name)
.NotEmpty()
.MaximumLength(100);
RuleFor(x => x.Password)
.NotEmpty()
.MinimumLength(8)
.Matches("[A-Z]").WithMessage("Password must contain uppercase letter")
.Matches("[a-z]").WithMessage("Password must contain lowercase letter")
.Matches("[0-9]").WithMessage("Password must contain digit")
.Matches("[^a-zA-Z0-9]").WithMessage("Password must contain special character");
}
}
public class CreateUserCommandHandler : IRequestHandler<CreateUserCommand, Result<UserDto>>
{
private readonly IUserRepository _userRepository;
private readonly IUnitOfWork _unitOfWork;
private readonly IPasswordHasher _passwordHasher;
public CreateUserCommandHandler(
IUserRepository userRepository,
IUnitOfWork unitOfWork,
IPasswordHasher passwordHasher)
{
_userRepository = userRepository;
_unitOfWork = unitOfWork;
_passwordHasher = passwordHasher;
}
public async Task<Result<UserDto>> Handle(
CreateUserCommand request,
CancellationToken cancellationToken)
{
if (await _userRepository.ExistsByEmailAsync(request.Email, cancellationToken))
{
return Result.Failure<UserDto>(DomainErrors.User.DuplicateEmail);
}
var userResult = User.Create(request.Email, request.Name);
if (userResult.IsFailure)
{
return Result.Failure<UserDto>(userResult.Error);
}
var user = userResult.Value;
await _userRepository.AddAsync(user, cancellationToken);
await _unitOfWork.SaveChangesAsync(cancellationToken);
return Result.Success(user.ToDto());
}
}
```
### Query
```csharp
namespace Application.Features.Users.Queries;
public record GetUserByIdQuery(Guid Id) : IRequest<Result<UserDto>>;
public class GetUserByIdQueryHandler : IRequestHandler<GetUserByIdQuery, Result<UserDto>>
{
private readonly IUserRepository _userRepository;
public GetUserByIdQueryHandler(IUserRepository userRepository)
{
_userRepository = userRepository;
}
public async Task<Result<UserDto>> Handle(
GetUserByIdQuery request,
CancellationToken cancellationToken)
{
var user = await _userRepository.GetByIdAsync(request.Id, cancellationToken);
if (user is null)
{
return Result.Failure<UserDto>(DomainErrors.User.NotFound);
}
return Result.Success(user.ToDto());
}
}
```
## API Controllers
```csharp
namespace Api.Controllers;
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
private readonly ISender _sender;
public UsersController(ISender sender)
{
_sender = sender;
}
[HttpGet("{id:guid}")]
[ProducesResponseType(typeof(UserDto), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetUser(Guid id, CancellationToken cancellationToken)
{
var result = await _sender.Send(new GetUserByIdQuery(id), cancellationToken);
return result.IsSuccess
? Ok(result.Value)
: NotFound(CreateProblemDetails(result.Error));
}
[HttpPost]
[ProducesResponseType(typeof(UserDto), StatusCodes.Status201Created)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status409Conflict)]
public async Task<IActionResult> CreateUser(
[FromBody] CreateUserRequest request,
CancellationToken cancellationToken)
{
var command = new CreateUserCommand
{
Email = request.Email,
Name = request.Name,
Password = request.Password
};
var result = await _sender.Send(command, cancellationToken);
if (result.IsFailure)
{
return result.Error.Code == "User.DuplicateEmail"
? Conflict(CreateProblemDetails(result.Error))
: BadRequest(CreateProblemDetails(result.Error));
}
return CreatedAtAction(
nameof(GetUser),
new { id = result.Value.Id },
result.Value);
}
private static ProblemDetails CreateProblemDetails(Error error) =>
new()
{
Title = error.Code,
Detail = error.Message,
Status = StatusCodes.Status400BadRequest
};
}
```
## Commands
```bash
# Build and test
dotnet build
dotnet test
dotnet test --collect:"XPlat Code Coverage"
# Run specific test project
dotnet test tests/Application.Tests
# Run with filter
dotnet test --filter "FullyQualifiedName~UserService"
# Generate coverage report (requires reportgenerator)
dotnet tool install -g dotnet-reportgenerator-globaltool
reportgenerator -reports:**/coverage.cobertura.xml -targetdir:coveragereport
# Run application
dotnet run --project src/Api
# EF Core migrations
dotnet ef migrations add InitialCreate --project src/Infrastructure --startup-project src/Api
dotnet ef database update --project src/Infrastructure --startup-project src/Api
```
## NuGet Dependencies
```xml
<!-- Domain.csproj - Minimal dependencies -->
<ItemGroup>
<PackageReference Include="FluentResults" Version="3.15.2" />
</ItemGroup>
<!-- Application.csproj -->
<ItemGroup>
<PackageReference Include="MediatR" Version="12.2.0" />
<PackageReference Include="FluentValidation" Version="11.9.0" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.9.0" />
</ItemGroup>
<!-- Infrastructure.csproj -->
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.0" />
</ItemGroup>
<!-- Test projects -->
<ItemGroup>
<PackageReference Include="xunit" Version="2.6.6" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.6" />
<PackageReference Include="Moq" Version="4.20.70" />
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.0" />
<PackageReference Include="coverlet.collector" Version="6.0.0" />
</ItemGroup>
```

View File

@@ -0,0 +1,686 @@
---
name: go-development
description: Go development patterns with testing, error handling, and idiomatic practices. Use when writing Go code.
---
# Go Development Skill
## Project Structure
```
project/
├── cmd/
│ └── server/
│ └── main.go # Application entry point
├── internal/
│ ├── domain/ # Business logic
│ │ ├── user.go
│ │ └── user_test.go
│ ├── repository/ # Data access
│ │ ├── user_repo.go
│ │ └── user_repo_test.go
│ ├── service/ # Application services
│ │ ├── user_service.go
│ │ └── user_service_test.go
│ └── handler/ # HTTP handlers
│ ├── user_handler.go
│ └── user_handler_test.go
├── pkg/ # Public packages
│ └── validation/
├── go.mod
├── go.sum
└── Makefile
```
## Testing Patterns
### Table-Driven Tests
```go
package user
import (
"testing"
)
func TestValidateEmail(t *testing.T) {
tests := []struct {
name string
email string
wantErr bool
}{
{
name: "valid email",
email: "user@example.com",
wantErr: false,
},
{
name: "missing @ symbol",
email: "userexample.com",
wantErr: true,
},
{
name: "empty email",
email: "",
wantErr: true,
},
{
name: "missing domain",
email: "user@",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateEmail(tt.email)
if (err != nil) != tt.wantErr {
t.Errorf("ValidateEmail(%q) error = %v, wantErr %v",
tt.email, err, tt.wantErr)
}
})
}
}
```
### Test Fixtures with Helper Functions
```go
package service_test
import (
"testing"
"myapp/internal/domain"
)
// Test helper - creates valid user with optional overrides
func newTestUser(t *testing.T, opts ...func(*domain.User)) *domain.User {
t.Helper()
user := &domain.User{
ID: "user-123",
Email: "test@example.com",
Name: "Test User",
Role: domain.RoleUser,
}
for _, opt := range opts {
opt(user)
}
return user
}
// Option functions for customization
func withEmail(email string) func(*domain.User) {
return func(u *domain.User) {
u.Email = email
}
}
func withRole(role domain.Role) func(*domain.User) {
return func(u *domain.User) {
u.Role = role
}
}
func TestUserService_CreateUser(t *testing.T) {
t.Run("creates user with valid data", func(t *testing.T) {
user := newTestUser(t)
svc := NewUserService(mockRepo)
result, err := svc.CreateUser(user)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result.ID == "" {
t.Error("expected user to have an ID")
}
})
t.Run("rejects user with invalid email", func(t *testing.T) {
user := newTestUser(t, withEmail("invalid-email"))
svc := NewUserService(mockRepo)
_, err := svc.CreateUser(user)
if err == nil {
t.Error("expected error for invalid email")
}
})
}
```
### Mocking with Interfaces
```go
package service
import (
"context"
"myapp/internal/domain"
)
// Repository interface for dependency injection
type UserRepository interface {
GetByID(ctx context.Context, id string) (*domain.User, error)
Create(ctx context.Context, user *domain.User) error
Update(ctx context.Context, user *domain.User) error
}
// Service with injected dependencies
type UserService struct {
repo UserRepository
}
func NewUserService(repo UserRepository) *UserService {
return &UserService{repo: repo}
}
// Test file
package service_test
import (
"context"
"errors"
"testing"
"myapp/internal/domain"
"myapp/internal/service"
)
// Mock implementation
type mockUserRepo struct {
users map[string]*domain.User
err error
}
func newMockUserRepo() *mockUserRepo {
return &mockUserRepo{
users: make(map[string]*domain.User),
}
}
func (m *mockUserRepo) GetByID(ctx context.Context, id string) (*domain.User, error) {
if m.err != nil {
return nil, m.err
}
user, ok := m.users[id]
if !ok {
return nil, errors.New("user not found")
}
return user, nil
}
func (m *mockUserRepo) Create(ctx context.Context, user *domain.User) error {
if m.err != nil {
return m.err
}
m.users[user.ID] = user
return nil
}
func (m *mockUserRepo) Update(ctx context.Context, user *domain.User) error {
if m.err != nil {
return m.err
}
m.users[user.ID] = user
return nil
}
```
## Error Handling
### Custom Error Types
```go
package domain
import (
"errors"
"fmt"
)
// Sentinel errors for comparison
var (
ErrNotFound = errors.New("not found")
ErrUnauthorized = errors.New("unauthorized")
ErrInvalidInput = errors.New("invalid input")
)
// Structured error with context
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation error on %s: %s", e.Field, e.Message)
}
// Error wrapping
func GetUser(ctx context.Context, id string) (*User, error) {
user, err := repo.GetByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("getting user %s: %w", id, err)
}
return user, nil
}
// Error checking
func handleError(err error) {
if errors.Is(err, ErrNotFound) {
// Handle not found
}
var validationErr *ValidationError
if errors.As(err, &validationErr) {
// Handle validation error
fmt.Printf("Field %s: %s\n", validationErr.Field, validationErr.Message)
}
}
```
### Result Pattern (Optional)
```go
package result
// Result type for explicit error handling
type Result[T any] struct {
value T
err error
}
func Ok[T any](value T) Result[T] {
return Result[T]{value: value}
}
func Err[T any](err error) Result[T] {
return Result[T]{err: err}
}
func (r Result[T]) IsOk() bool {
return r.err == nil
}
func (r Result[T]) IsErr() bool {
return r.err != nil
}
func (r Result[T]) Unwrap() (T, error) {
return r.value, r.err
}
func (r Result[T]) UnwrapOr(defaultValue T) T {
if r.err != nil {
return defaultValue
}
return r.value
}
// Usage
func ProcessPayment(amount float64) Result[*Receipt] {
if amount <= 0 {
return Err[*Receipt](ErrInvalidInput)
}
receipt := &Receipt{Amount: amount}
return Ok(receipt)
}
```
## HTTP Handlers
### Chi Router Pattern
```go
package handler
import (
"encoding/json"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"myapp/internal/service"
)
type UserHandler struct {
userService *service.UserService
}
func NewUserHandler(userService *service.UserService) *UserHandler {
return &UserHandler{userService: userService}
}
func (h *UserHandler) Routes() chi.Router {
r := chi.NewRouter()
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Get("/", h.ListUsers)
r.Post("/", h.CreateUser)
r.Get("/{userID}", h.GetUser)
r.Put("/{userID}", h.UpdateUser)
r.Delete("/{userID}", h.DeleteUser)
return r
}
func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {
userID := chi.URLParam(r, "userID")
user, err := h.userService.GetUser(r.Context(), userID)
if err != nil {
if errors.Is(err, domain.ErrNotFound) {
http.Error(w, "User not found", http.StatusNotFound)
return
}
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
respondJSON(w, http.StatusOK, user)
}
func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
var req CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
if err := req.Validate(); err != nil {
respondJSON(w, http.StatusBadRequest, map[string]string{
"error": err.Error(),
})
return
}
user, err := h.userService.CreateUser(r.Context(), req.ToUser())
if err != nil {
http.Error(w, "Failed to create user", http.StatusInternalServerError)
return
}
respondJSON(w, http.StatusCreated, user)
}
func respondJSON(w http.ResponseWriter, status int, data any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
```
### Request Validation
```go
package handler
import (
"regexp"
)
var emailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
type CreateUserRequest struct {
Email string `json:"email"`
Name string `json:"name"`
}
func (r *CreateUserRequest) Validate() error {
if r.Email == "" {
return &domain.ValidationError{
Field: "email",
Message: "email is required",
}
}
if !emailRegex.MatchString(r.Email) {
return &domain.ValidationError{
Field: "email",
Message: "invalid email format",
}
}
if r.Name == "" {
return &domain.ValidationError{
Field: "name",
Message: "name is required",
}
}
if len(r.Name) > 100 {
return &domain.ValidationError{
Field: "name",
Message: "name must be 100 characters or less",
}
}
return nil
}
func (r *CreateUserRequest) ToUser() *domain.User {
return &domain.User{
Email: r.Email,
Name: r.Name,
}
}
```
## Context Usage
```go
package service
import (
"context"
"time"
)
// Context with timeout
func (s *UserService) ProcessOrder(ctx context.Context, orderID string) error {
// Create a timeout context
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
// Check context before expensive operations
select {
case <-ctx.Done():
return ctx.Err()
default:
}
// Process order...
return s.repo.UpdateOrder(ctx, orderID)
}
// Context values for request-scoped data
type contextKey string
const (
userIDKey contextKey = "userID"
requestIDKey contextKey = "requestID"
)
func WithUserID(ctx context.Context, userID string) context.Context {
return context.WithValue(ctx, userIDKey, userID)
}
func UserIDFromContext(ctx context.Context) (string, bool) {
userID, ok := ctx.Value(userIDKey).(string)
return userID, ok
}
```
## Concurrency Patterns
### Worker Pool
```go
package worker
import (
"context"
"sync"
)
type Job func(ctx context.Context) error
type Pool struct {
workers int
jobs chan Job
results chan error
wg sync.WaitGroup
}
func NewPool(workers int) *Pool {
return &Pool{
workers: workers,
jobs: make(chan Job, workers*2),
results: make(chan error, workers*2),
}
}
func (p *Pool) Start(ctx context.Context) {
for i := 0; i < p.workers; i++ {
p.wg.Add(1)
go p.worker(ctx)
}
}
func (p *Pool) worker(ctx context.Context) {
defer p.wg.Done()
for {
select {
case <-ctx.Done():
return
case job, ok := <-p.jobs:
if !ok {
return
}
if err := job(ctx); err != nil {
p.results <- err
}
}
}
}
func (p *Pool) Submit(job Job) {
p.jobs <- job
}
func (p *Pool) Close() {
close(p.jobs)
p.wg.Wait()
close(p.results)
}
```
### Error Group
```go
package service
import (
"context"
"golang.org/x/sync/errgroup"
)
func (s *OrderService) ProcessBatch(ctx context.Context, orderIDs []string) error {
g, ctx := errgroup.WithContext(ctx)
// Limit concurrency
g.SetLimit(10)
for _, orderID := range orderIDs {
orderID := orderID // Capture for goroutine
g.Go(func() error {
return s.ProcessOrder(ctx, orderID)
})
}
return g.Wait()
}
```
## Commands
```bash
# Testing
go test ./... # Run all tests
go test -v ./... # Verbose output
go test -race ./... # Race detection
go test -cover ./... # Coverage summary
go test -coverprofile=coverage.out ./... # Coverage file
go tool cover -html=coverage.out # HTML coverage report
# Linting
go vet ./... # Built-in checks
golangci-lint run # Comprehensive linting
# Building
go build -o bin/server ./cmd/server # Build binary
go build -ldflags="-s -w" -o bin/server ./cmd/server # Smaller binary
# Dependencies
go mod tidy # Clean up dependencies
go mod verify # Verify dependencies
# Code generation
go generate ./... # Run generate directives
```
## Anti-Patterns to Avoid
```go
// ❌ BAD - Returning nil error with nil value
func GetUser(id string) (*User, error) {
user := db.Find(id)
return user, nil // user might be nil!
}
// ✅ GOOD - Explicit error for not found
func GetUser(id string) (*User, error) {
user := db.Find(id)
if user == nil {
return nil, ErrNotFound
}
return user, nil
}
// ❌ BAD - Ignoring errors
result, _ := SomeFunction()
// ✅ GOOD - Handle or propagate errors
result, err := SomeFunction()
if err != nil {
return fmt.Errorf("calling SomeFunction: %w", err)
}
// ❌ BAD - Naked goroutine without error handling
go processItem(item)
// ✅ GOOD - Error handling with channels or error groups
g, ctx := errgroup.WithContext(ctx)
g.Go(func() error {
return processItem(ctx, item)
})
if err := g.Wait(); err != nil {
return err
}
// ❌ BAD - Mutex embedded in struct
type Service struct {
sync.Mutex
data map[string]string
}
// ✅ GOOD - Private mutex
type Service struct {
mu sync.Mutex
data map[string]string
}
```

View File

@@ -0,0 +1,675 @@
---
name: java-development
description: Java development patterns with Spring Boot, JUnit 5, and modern Java practices. Use when writing Java code.
---
# Java Development Skill
## Project Structure (Spring Boot)
```
project/
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/mycompany/myapp/
│ │ │ ├── MyApplication.java
│ │ │ ├── domain/
│ │ │ │ ├── User.java
│ │ │ │ └── Order.java
│ │ │ ├── repository/
│ │ │ │ └── UserRepository.java
│ │ │ ├── service/
│ │ │ │ └── UserService.java
│ │ │ ├── controller/
│ │ │ │ └── UserController.java
│ │ │ └── config/
│ │ │ └── SecurityConfig.java
│ │ └── resources/
│ │ ├── application.yml
│ │ └── application-test.yml
│ └── test/
│ └── java/
│ └── com/mycompany/myapp/
│ ├── service/
│ │ └── UserServiceTest.java
│ └── controller/
│ └── UserControllerTest.java
├── pom.xml
└── README.md
```
## Testing with JUnit 5
### Unit Tests with Mockito
```java
package com.mycompany.myapp.service;
import com.mycompany.myapp.domain.User;
import com.mycompany.myapp.repository.UserRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Optional;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@Nested
@DisplayName("createUser")
class CreateUser {
@Test
@DisplayName("should create user with valid data")
void shouldCreateUserWithValidData() {
// Given
var request = UserFixtures.createUserRequest();
var savedUser = UserFixtures.user();
when(userRepository.save(any(User.class))).thenReturn(savedUser);
// When
var result = userService.createUser(request);
// Then
assertThat(result.getId()).isNotNull();
assertThat(result.getEmail()).isEqualTo(request.getEmail());
verify(userRepository).save(any(User.class));
}
@Test
@DisplayName("should throw exception for duplicate email")
void shouldThrowExceptionForDuplicateEmail() {
// Given
var request = UserFixtures.createUserRequest();
when(userRepository.existsByEmail(request.getEmail())).thenReturn(true);
// When/Then
assertThatThrownBy(() -> userService.createUser(request))
.isInstanceOf(DuplicateEmailException.class)
.hasMessageContaining(request.getEmail());
}
}
@Nested
@DisplayName("getUserById")
class GetUserById {
@Test
@DisplayName("should return user when found")
void shouldReturnUserWhenFound() {
// Given
var user = UserFixtures.user();
when(userRepository.findById(user.getId())).thenReturn(Optional.of(user));
// When
var result = userService.getUserById(user.getId());
// Then
assertThat(result).isPresent();
assertThat(result.get().getId()).isEqualTo(user.getId());
}
@Test
@DisplayName("should return empty when user not found")
void shouldReturnEmptyWhenNotFound() {
// Given
when(userRepository.findById(any())).thenReturn(Optional.empty());
// When
var result = userService.getUserById("unknown-id");
// Then
assertThat(result).isEmpty();
}
}
}
```
### Test Fixtures
```java
package com.mycompany.myapp.fixtures;
import com.mycompany.myapp.domain.User;
import com.mycompany.myapp.dto.CreateUserRequest;
import java.time.Instant;
import java.util.UUID;
public final class UserFixtures {
private UserFixtures() {}
public static User user() {
return user(builder -> {});
}
public static User user(UserCustomizer customizer) {
var builder = User.builder()
.id(UUID.randomUUID().toString())
.email("test@example.com")
.name("Test User")
.role(Role.USER)
.createdAt(Instant.now())
.updatedAt(Instant.now());
customizer.customize(builder);
return builder.build();
}
public static CreateUserRequest createUserRequest() {
return createUserRequest(builder -> {});
}
public static CreateUserRequest createUserRequest(CreateUserRequestCustomizer customizer) {
var builder = CreateUserRequest.builder()
.email("newuser@example.com")
.name("New User")
.password("SecurePass123!");
customizer.customize(builder);
return builder.build();
}
@FunctionalInterface
public interface UserCustomizer {
void customize(User.UserBuilder builder);
}
@FunctionalInterface
public interface CreateUserRequestCustomizer {
void customize(CreateUserRequest.CreateUserRequestBuilder builder);
}
}
// Usage in tests
var adminUser = UserFixtures.user(builder -> builder.role(Role.ADMIN));
var customRequest = UserFixtures.createUserRequest(builder ->
builder.email("custom@example.com"));
```
### Parameterized Tests
```java
package com.mycompany.myapp.validation;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.NullAndEmptySource;
import org.junit.jupiter.params.provider.ValueSource;
import static org.assertj.core.api.Assertions.*;
class EmailValidatorTest {
private final EmailValidator validator = new EmailValidator();
@ParameterizedTest
@DisplayName("should accept valid emails")
@ValueSource(strings = {
"user@example.com",
"user.name@example.com",
"user+tag@example.co.uk"
})
void shouldAcceptValidEmails(String email) {
assertThat(validator.isValid(email)).isTrue();
}
@ParameterizedTest
@DisplayName("should reject invalid emails")
@ValueSource(strings = {
"invalid",
"missing@domain",
"@nodomain.com",
"spaces in@email.com"
})
void shouldRejectInvalidEmails(String email) {
assertThat(validator.isValid(email)).isFalse();
}
@ParameterizedTest
@DisplayName("should reject null and empty emails")
@NullAndEmptySource
void shouldRejectNullAndEmpty(String email) {
assertThat(validator.isValid(email)).isFalse();
}
@ParameterizedTest
@DisplayName("should validate password strength")
@CsvSource({
"password,false",
"Password1,false",
"Password1!,true",
"P@ssw0rd,true"
})
void shouldValidatePasswordStrength(String password, boolean expected) {
assertThat(validator.isStrongPassword(password)).isEqualTo(expected);
}
}
```
### Integration Tests with Spring
```java
package com.mycompany.myapp.controller;
import com.mycompany.myapp.fixtures.UserFixtures;
import com.mycompany.myapp.repository.UserRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
class UserControllerIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private UserRepository userRepository;
@BeforeEach
void setUp() {
userRepository.deleteAll();
}
@Test
@DisplayName("POST /api/users creates user and returns 201")
void createUserReturnsCreated() throws Exception {
var request = """
{
"email": "new@example.com",
"name": "New User",
"password": "SecurePass123!"
}
""";
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(request))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").isNotEmpty())
.andExpect(jsonPath("$.email").value("new@example.com"))
.andExpect(jsonPath("$.name").value("New User"));
}
@Test
@DisplayName("GET /api/users/{id} returns user when found")
void getUserReturnsUserWhenFound() throws Exception {
var user = userRepository.save(UserFixtures.user());
mockMvc.perform(get("/api/users/{id}", user.getId()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(user.getId()))
.andExpect(jsonPath("$.email").value(user.getEmail()));
}
@Test
@DisplayName("GET /api/users/{id} returns 404 when not found")
void getUserReturns404WhenNotFound() throws Exception {
mockMvc.perform(get("/api/users/{id}", "unknown-id"))
.andExpect(status().isNotFound());
}
}
```
## Domain Models with Records
```java
package com.mycompany.myapp.domain;
import jakarta.validation.constraints.*;
import java.time.Instant;
// Immutable domain model using records (Java 17+)
public record User(
String id,
@NotBlank @Email String email,
@NotBlank @Size(max = 100) String name,
Role role,
Instant createdAt,
Instant updatedAt
) {
// Compact constructor for validation
public User {
if (email != null) {
email = email.toLowerCase().trim();
}
}
// Static factory methods
public static User create(String email, String name) {
var now = Instant.now();
return new User(
UUID.randomUUID().toString(),
email,
name,
Role.USER,
now,
now
);
}
// Wither methods for immutable updates
public User withName(String newName) {
return new User(id, email, newName, role, createdAt, Instant.now());
}
public User withRole(Role newRole) {
return new User(id, email, name, newRole, createdAt, Instant.now());
}
}
// For mutable entities (JPA)
@Entity
@Table(name = "users")
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserEntity {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private String id;
@Column(unique = true, nullable = false)
private String email;
@Column(nullable = false)
private String name;
@Enumerated(EnumType.STRING)
private Role role;
@Column(name = "created_at")
private Instant createdAt;
@Column(name = "updated_at")
private Instant updatedAt;
@PrePersist
void onCreate() {
createdAt = Instant.now();
updatedAt = createdAt;
}
@PreUpdate
void onUpdate() {
updatedAt = Instant.now();
}
}
```
## Service Layer
```java
package com.mycompany.myapp.service;
import com.mycompany.myapp.domain.User;
import com.mycompany.myapp.dto.CreateUserRequest;
import com.mycompany.myapp.exception.DuplicateEmailException;
import com.mycompany.myapp.exception.UserNotFoundException;
import com.mycompany.myapp.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Optional;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
@Transactional
public User createUser(CreateUserRequest request) {
if (userRepository.existsByEmail(request.getEmail())) {
throw new DuplicateEmailException(request.getEmail());
}
var user = User.create(
request.getEmail(),
request.getName()
);
return userRepository.save(user);
}
public Optional<User> getUserById(String id) {
return userRepository.findById(id);
}
public User getUserByIdOrThrow(String id) {
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
}
@Transactional
public User updateUser(String id, UpdateUserRequest request) {
var user = getUserByIdOrThrow(id);
var updatedUser = user
.withName(request.getName())
.withRole(request.getRole());
return userRepository.save(updatedUser);
}
@Transactional
public void deleteUser(String id) {
if (!userRepository.existsById(id)) {
throw new UserNotFoundException(id);
}
userRepository.deleteById(id);
}
}
```
## REST Controllers
```java
package com.mycompany.myapp.controller;
import com.mycompany.myapp.dto.*;
import com.mycompany.myapp.service.UserService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
private final UserMapper userMapper;
@GetMapping
public List<UserResponse> listUsers() {
return userService.getAllUsers().stream()
.map(userMapper::toResponse)
.toList();
}
@GetMapping("/{id}")
public ResponseEntity<UserResponse> getUser(@PathVariable String id) {
return userService.getUserById(id)
.map(userMapper::toResponse)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public UserResponse createUser(@Valid @RequestBody CreateUserRequest request) {
var user = userService.createUser(request);
return userMapper.toResponse(user);
}
@PutMapping("/{id}")
public UserResponse updateUser(
@PathVariable String id,
@Valid @RequestBody UpdateUserRequest request) {
var user = userService.updateUser(id, request);
return userMapper.toResponse(user);
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteUser(@PathVariable String id) {
userService.deleteUser(id);
}
}
```
## Exception Handling
```java
package com.mycompany.myapp.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.net.URI;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(UserNotFoundException.class)
public ProblemDetail handleUserNotFound(UserNotFoundException ex) {
var problem = ProblemDetail.forStatusAndDetail(
HttpStatus.NOT_FOUND,
ex.getMessage()
);
problem.setTitle("User Not Found");
problem.setType(URI.create("https://api.myapp.com/errors/user-not-found"));
return problem;
}
@ExceptionHandler(DuplicateEmailException.class)
public ProblemDetail handleDuplicateEmail(DuplicateEmailException ex) {
var problem = ProblemDetail.forStatusAndDetail(
HttpStatus.CONFLICT,
ex.getMessage()
);
problem.setTitle("Duplicate Email");
problem.setType(URI.create("https://api.myapp.com/errors/duplicate-email"));
return problem;
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ProblemDetail handleValidation(MethodArgumentNotValidException ex) {
var problem = ProblemDetail.forStatusAndDetail(
HttpStatus.BAD_REQUEST,
"Validation failed"
);
problem.setTitle("Validation Error");
var errors = ex.getBindingResult().getFieldErrors().stream()
.map(e -> new ValidationError(e.getField(), e.getDefaultMessage()))
.toList();
problem.setProperty("errors", errors);
return problem;
}
record ValidationError(String field, String message) {}
}
```
## Commands
```bash
# Maven
mvn clean install # Build and test
mvn test # Run tests only
mvn verify # Run integration tests
mvn spring-boot:run # Run application
mvn jacoco:report # Generate coverage report
# Gradle
./gradlew build # Build and test
./gradlew test # Run tests only
./gradlew integrationTest # Run integration tests
./gradlew bootRun # Run application
./gradlew jacocoTestReport # Generate coverage report
# Common flags
-DskipTests # Skip tests
-Dspring.profiles.active=test # Set profile
```
## Dependencies (pom.xml)
```xml
<dependencies>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
```

View File

@@ -0,0 +1,446 @@
---
name: python-fastapi
description: Python development with FastAPI, pytest, Ruff, Pydantic v2, and SQLAlchemy async patterns. Use when writing Python code, tests, or APIs.
---
# Python Development Skill
## Project Structure
```
src/
├── backend/
│ ├── __init__.py
│ ├── main.py # FastAPI app entry
│ ├── config.py # Pydantic Settings
│ ├── domains/
│ │ └── users/
│ │ ├── __init__.py
│ │ ├── router.py # API routes
│ │ ├── service.py # Business logic
│ │ ├── models.py # SQLAlchemy models
│ │ └── schemas.py # Pydantic schemas
│ └── shared/
│ ├── database.py # DB session management
│ └── models/ # Base models
tests/
├── backend/
│ ├── conftest.py # Shared fixtures
│ └── domains/
│ └── users/
│ └── test_service.py
```
## Testing with pytest
### Configuration (pyproject.toml)
```toml
[tool.pytest.ini_options]
testpaths = ["tests"]
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"
addopts = [
"-v",
"--strict-markers",
"--cov=src",
"--cov-report=term-missing",
"--cov-fail-under=80"
]
[tool.coverage.run]
branch = true
source = ["src"]
omit = ["*/__init__.py", "*/migrations/*"]
```
### Async Test Fixtures
```python
# tests/conftest.py
import pytest
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.orm import sessionmaker
from src.backend.shared.models.base import Base
@pytest.fixture
async def db_session():
"""Create in-memory SQLite for fast tests."""
engine = create_async_engine(
"sqlite+aiosqlite:///:memory:",
echo=False,
)
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
async_session = sessionmaker(
engine, class_=AsyncSession, expire_on_commit=False
)
async with async_session() as session:
yield session
await session.rollback()
await engine.dispose()
```
### Factory Functions for Test Data
```python
# tests/factories.py
from typing import Any
from src.backend.domains.users.schemas import UserCreate
def get_mock_user_create(overrides: dict[str, Any] | None = None) -> UserCreate:
"""Factory for UserCreate with sensible defaults."""
defaults = {
"email": "test@example.com",
"name": "Test User",
"password": "securepassword123",
}
return UserCreate(**(defaults | (overrides or {})))
def get_mock_user_dict(overrides: dict[str, Any] | None = None) -> dict[str, Any]:
"""Factory for raw user dict."""
defaults = {
"id": "user_123",
"email": "test@example.com",
"name": "Test User",
"is_active": True,
}
return defaults | (overrides or {})
```
### Test Patterns
```python
# tests/backend/domains/users/test_service.py
import pytest
from unittest.mock import AsyncMock
from src.backend.domains.users.service import UserService
from tests.factories import get_mock_user_create
class TestUserService:
"""Test user service behavior through public API."""
async def test_create_user_returns_user_with_id(self, db_session):
"""Creating a user should return user with generated ID."""
service = UserService(db_session)
user_data = get_mock_user_create()
result = await service.create_user(user_data)
assert result.id is not None
assert result.email == user_data.email
async def test_create_user_hashes_password(self, db_session):
"""Password should be hashed, not stored in plain text."""
service = UserService(db_session)
user_data = get_mock_user_create({"password": "mypassword"})
result = await service.create_user(user_data)
assert result.hashed_password != "mypassword"
assert len(result.hashed_password) > 50 # Hashed
async def test_create_duplicate_email_raises_error(self, db_session):
"""Duplicate email should raise ValueError."""
service = UserService(db_session)
user_data = get_mock_user_create()
await service.create_user(user_data)
with pytest.raises(ValueError, match="Email already exists"):
await service.create_user(user_data)
async def test_get_user_not_found_returns_none(self, db_session):
"""Non-existent user should return None."""
service = UserService(db_session)
result = await service.get_user("nonexistent_id")
assert result is None
```
## Pydantic v2 Patterns
### Schemas with Validation
```python
# src/backend/domains/users/schemas.py
from datetime import datetime
from pydantic import BaseModel, EmailStr, Field, field_validator
class UserBase(BaseModel):
"""Base user schema with shared fields."""
email: EmailStr
name: str = Field(..., min_length=1, max_length=100)
class UserCreate(UserBase):
"""Schema for creating a user."""
password: str = Field(..., min_length=8)
@field_validator("password")
@classmethod
def password_must_be_strong(cls, v: str) -> str:
if not any(c.isupper() for c in v):
raise ValueError("Password must contain uppercase letter")
if not any(c.isdigit() for c in v):
raise ValueError("Password must contain a digit")
return v
class UserResponse(UserBase):
"""Schema for user responses (no password)."""
id: str
is_active: bool
created_at: datetime
model_config = {"from_attributes": True}
```
### Configuration with Pydantic Settings
```python
# src/backend/config.py
from functools import lru_cache
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
"""Application settings from environment."""
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
)
# Database
database_url: str
database_pool_size: int = 5
# Redis
redis_url: str = "redis://localhost:6379"
# Security
secret_key: str
access_token_expire_minutes: int = 30
# AWS
aws_region: str = "eu-west-2"
@lru_cache
def get_settings() -> Settings:
return Settings()
```
## FastAPI Patterns
### Router with Dependency Injection
```python
# src/backend/domains/users/router.py
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from src.backend.shared.database import get_db_session
from .schemas import UserCreate, UserResponse
from .service import UserService
router = APIRouter(prefix="/users", tags=["users"])
def get_user_service(
session: AsyncSession = Depends(get_db_session),
) -> UserService:
return UserService(session)
@router.post("/", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
async def create_user(
user_data: UserCreate,
service: UserService = Depends(get_user_service),
) -> UserResponse:
"""Create a new user."""
try:
return await service.create_user(user_data)
except ValueError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
@router.get("/{user_id}", response_model=UserResponse)
async def get_user(
user_id: str,
service: UserService = Depends(get_user_service),
) -> UserResponse:
"""Get user by ID."""
user = await service.get_user(user_id)
if not user:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
return user
```
## SQLAlchemy 2.0 Async Patterns
### Model Definition
```python
# src/backend/domains/users/models.py
from datetime import datetime
from sqlalchemy import String, Boolean, DateTime
from sqlalchemy.orm import Mapped, mapped_column
from src.backend.shared.models.base import Base
class User(Base):
__tablename__ = "users"
id: Mapped[str] = mapped_column(String(36), primary_key=True)
email: Mapped[str] = mapped_column(String(255), unique=True, index=True)
name: Mapped[str] = mapped_column(String(100))
hashed_password: Mapped[str] = mapped_column(String(255))
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
created_at: Mapped[datetime] = mapped_column(
DateTime, default=datetime.utcnow
)
```
### Async Repository Pattern
```python
# src/backend/domains/users/repository.py
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from .models import User
class UserRepository:
def __init__(self, session: AsyncSession):
self._session = session
async def get_by_id(self, user_id: str) -> User | None:
result = await self._session.execute(
select(User).where(User.id == user_id)
)
return result.scalar_one_or_none()
async def get_by_email(self, email: str) -> User | None:
result = await self._session.execute(
select(User).where(User.email == email)
)
return result.scalar_one_or_none()
async def create(self, user: User) -> User:
self._session.add(user)
await self._session.commit()
await self._session.refresh(user)
return user
```
## Ruff Configuration
```toml
# pyproject.toml
[tool.ruff]
line-length = 120
target-version = "py311"
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"F", # pyflakes
"I", # isort
"UP", # pyupgrade
"B", # flake8-bugbear
"SIM", # flake8-simplify
"ASYNC", # flake8-async
]
ignore = [
"B008", # function-call-in-default-argument (FastAPI Depends)
"E501", # line-too-long (handled by formatter)
]
[tool.ruff.lint.per-file-ignores]
"__init__.py" = ["F401"] # unused imports (re-exports)
"tests/*" = ["S101"] # assert allowed in tests
```
## Commands
```bash
# Testing
pytest # Run all tests
pytest tests/backend/domains/users/ # Run specific directory
pytest -k "test_create" # Run tests matching pattern
pytest --cov=src --cov-report=html # Coverage with HTML report
pytest -x # Stop on first failure
pytest --lf # Run last failed tests
# Linting & Formatting
ruff check . # Lint check
ruff check . --fix # Auto-fix issues
ruff format . # Format code
ruff format . --check # Check formatting
# Type Checking
mypy src # Full type check
pyright src # Alternative type checker
# Development
uvicorn src.backend.main:app --reload # Dev server
alembic revision --autogenerate -m "msg" # Create migration
alembic upgrade head # Apply migrations
```
## Anti-Patterns to Avoid
```python
# BAD: Mutable default argument
def process_items(items: list = []): # Creates shared state!
items.append("new")
return items
# GOOD: Use None default
def process_items(items: list | None = None) -> list:
if items is None:
items = []
return [*items, "new"]
# BAD: Bare except
try:
risky_operation()
except: # Catches everything including SystemExit!
pass
# GOOD: Specific exceptions
try:
risky_operation()
except ValueError as e:
logger.error(f"Validation failed: {e}")
raise
# BAD: String formatting in SQL (injection risk!)
query = f"SELECT * FROM users WHERE email = '{email}'"
# GOOD: Parameterized query
stmt = select(User).where(User.email == email)
# BAD: Sync in async context
async def get_data():
return requests.get(url) # Blocks event loop!
# GOOD: Use async client
async def get_data():
async with httpx.AsyncClient() as client:
return await client.get(url)
```

View File

@@ -0,0 +1,574 @@
---
name: rust-async
description: Rust development with Tokio async runtime, cargo test, clippy, and systems programming patterns. Use when writing Rust code, agents, or system utilities.
---
# Rust Development Skill
## Project Structure
```
my-agent/
├── Cargo.toml
├── Cargo.lock
├── src/
│ ├── main.rs # Binary entry point
│ ├── lib.rs # Library root (if dual crate)
│ ├── config.rs # Configuration handling
│ ├── error.rs # Error types
│ ├── client/
│ │ ├── mod.rs
│ │ └── http.rs
│ └── discovery/
│ ├── mod.rs
│ ├── system.rs
│ └── network.rs
├── tests/
│ └── integration/
│ └── discovery_test.rs
└── benches/
└── performance.rs
```
## Cargo Configuration
```toml
# Cargo.toml
[package]
name = "my-agent"
version = "0.1.0"
edition = "2021"
rust-version = "1.75"
[dependencies]
# Async runtime
tokio = { version = "1.35", features = ["full"] }
# HTTP client
reqwest = { version = "0.11", features = ["json", "socks"] }
# Serialization
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
# Error handling
thiserror = "1.0"
anyhow = "1.0"
# Logging
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
# System info
sysinfo = "0.30"
[dev-dependencies]
tokio-test = "0.4"
mockall = "0.12"
tempfile = "3.10"
[profile.release]
opt-level = "z" # Optimize for size
lto = true # Link-time optimization
codegen-units = 1 # Better optimization
strip = true # Strip symbols
panic = "abort" # Smaller binary
[lints.rust]
unsafe_code = "forbid"
[lints.clippy]
all = "deny"
pedantic = "warn"
nursery = "warn"
```
## Error Handling
### Custom Error Types
```rust
// src/error.rs
use thiserror::Error;
#[derive(Error, Debug)]
pub enum AgentError {
#[error("Configuration error: {0}")]
Config(String),
#[error("Network error: {0}")]
Network(#[from] reqwest::Error),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Serialization error: {0}")]
Serialization(#[from] serde_json::Error),
#[error("Discovery failed: {message}")]
Discovery { message: String, source: Option<Box<dyn std::error::Error + Send + Sync>> },
#[error("Connection timeout after {duration_secs}s")]
Timeout { duration_secs: u64 },
}
pub type Result<T> = std::result::Result<T, AgentError>;
```
### Result Pattern Usage
```rust
// src/client/http.rs
use crate::error::{AgentError, Result};
pub struct HttpClient {
client: reqwest::Client,
base_url: String,
}
impl HttpClient {
pub fn new(base_url: &str, timeout_secs: u64) -> Result<Self> {
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(timeout_secs))
.build()
.map_err(AgentError::Network)?;
Ok(Self {
client,
base_url: base_url.to_string(),
})
}
pub async fn get<T: serde::de::DeserializeOwned>(&self, path: &str) -> Result<T> {
let url = format!("{}{}", self.base_url, path);
let response = self
.client
.get(&url)
.send()
.await?
.error_for_status()?
.json::<T>()
.await?;
Ok(response)
}
pub async fn post<T, R>(&self, path: &str, body: &T) -> Result<R>
where
T: serde::Serialize,
R: serde::de::DeserializeOwned,
{
let url = format!("{}{}", self.base_url, path);
let response = self
.client
.post(&url)
.json(body)
.send()
.await?
.error_for_status()?
.json::<R>()
.await?;
Ok(response)
}
}
```
## Async Patterns with Tokio
### Main Entry Point
```rust
// src/main.rs
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
mod config;
mod error;
mod client;
mod discovery;
use crate::config::Config;
use crate::error::Result;
#[tokio::main]
async fn main() -> Result<()> {
// Initialize tracing
tracing_subscriber::registry()
.with(tracing_subscriber::EnvFilter::new(
std::env::var("RUST_LOG").unwrap_or_else(|_| "info".into()),
))
.with(tracing_subscriber::fmt::layer())
.init();
let config = Config::from_env()?;
tracing::info!(version = env!("CARGO_PKG_VERSION"), "Starting agent");
run(config).await
}
async fn run(config: Config) -> Result<()> {
let client = client::HttpClient::new(&config.server_url, config.timeout_secs)?;
// Main loop with graceful shutdown
let mut interval = tokio::time::interval(config.poll_interval);
loop {
tokio::select! {
_ = interval.tick() => {
if let Err(e) = discovery::collect_and_send(&client).await {
tracing::error!(error = %e, "Discovery cycle failed");
}
}
_ = tokio::signal::ctrl_c() => {
tracing::info!("Shutdown signal received");
break;
}
}
}
Ok(())
}
```
### Concurrent Operations
```rust
// src/discovery/mod.rs
use tokio::task::JoinSet;
use crate::error::Result;
pub async fn discover_all() -> Result<SystemInfo> {
let mut tasks = JoinSet::new();
// Spawn concurrent discovery tasks
tasks.spawn(async { ("cpu", discover_cpu().await) });
tasks.spawn(async { ("memory", discover_memory().await) });
tasks.spawn(async { ("disk", discover_disk().await) });
tasks.spawn(async { ("network", discover_network().await) });
let mut info = SystemInfo::default();
// Collect results as they complete
while let Some(result) = tasks.join_next().await {
match result {
Ok((name, Ok(data))) => {
tracing::debug!(component = name, "Discovery completed");
info.merge(name, data);
}
Ok((name, Err(e))) => {
tracing::warn!(component = name, error = %e, "Discovery failed");
}
Err(e) => {
tracing::error!(error = %e, "Task panicked");
}
}
}
Ok(info)
}
```
## Testing Patterns
### Unit Tests
```rust
// src/discovery/system.rs
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct CpuInfo {
pub cores: usize,
pub usage_percent: f32,
pub model: String,
}
impl CpuInfo {
pub fn is_high_usage(&self) -> bool {
self.usage_percent > 80.0
}
}
#[cfg(test)]
mod tests {
use super::*;
fn get_mock_cpu_info(overrides: Option<CpuInfo>) -> CpuInfo {
let default = CpuInfo {
cores: 4,
usage_percent: 25.0,
model: "Test CPU".to_string(),
};
overrides.unwrap_or(default)
}
#[test]
fn test_is_high_usage_returns_true_above_threshold() {
let cpu = get_mock_cpu_info(Some(CpuInfo {
usage_percent: 85.0,
..get_mock_cpu_info(None)
}));
assert!(cpu.is_high_usage());
}
#[test]
fn test_is_high_usage_returns_false_below_threshold() {
let cpu = get_mock_cpu_info(Some(CpuInfo {
usage_percent: 50.0,
..get_mock_cpu_info(None)
}));
assert!(!cpu.is_high_usage());
}
#[test]
fn test_is_high_usage_returns_false_at_boundary() {
let cpu = get_mock_cpu_info(Some(CpuInfo {
usage_percent: 80.0,
..get_mock_cpu_info(None)
}));
assert!(!cpu.is_high_usage());
}
}
```
### Async Tests
```rust
// tests/integration/client_test.rs
use tokio_test::block_on;
use my_agent::client::HttpClient;
#[tokio::test]
async fn test_client_handles_timeout() {
let client = HttpClient::new("http://localhost:9999", 1).unwrap();
let result: Result<serde_json::Value, _> = client.get("/test").await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_concurrent_requests_complete() {
let client = HttpClient::new("https://httpbin.org", 30).unwrap();
let handles: Vec<_> = (0..5)
.map(|i| {
let client = client.clone();
tokio::spawn(async move {
client.get::<serde_json::Value>(&format!("/delay/{}", i % 2)).await
})
})
.collect();
for handle in handles {
let result = handle.await.unwrap();
assert!(result.is_ok());
}
}
```
### Mocking with Mockall
```rust
// src/discovery/mod.rs
#[cfg_attr(test, mockall::automock)]
pub trait SystemDiscovery {
fn get_cpu_info(&self) -> crate::error::Result<CpuInfo>;
fn get_memory_info(&self) -> crate::error::Result<MemoryInfo>;
}
// src/discovery/collector.rs
pub struct Collector<D: SystemDiscovery> {
discovery: D,
}
impl<D: SystemDiscovery> Collector<D> {
pub fn new(discovery: D) -> Self {
Self { discovery }
}
pub fn collect(&self) -> crate::error::Result<SystemSnapshot> {
let cpu = self.discovery.get_cpu_info()?;
let memory = self.discovery.get_memory_info()?;
Ok(SystemSnapshot { cpu, memory })
}
}
#[cfg(test)]
mod tests {
use super::*;
use mockall::predicate::*;
#[test]
fn test_collector_combines_cpu_and_memory() {
let mut mock = MockSystemDiscovery::new();
mock.expect_get_cpu_info()
.times(1)
.returning(|| Ok(CpuInfo {
cores: 4,
usage_percent: 50.0,
model: "Mock CPU".to_string(),
}));
mock.expect_get_memory_info()
.times(1)
.returning(|| Ok(MemoryInfo {
total_gb: 16.0,
available_gb: 8.0,
}));
let collector = Collector::new(mock);
let snapshot = collector.collect().unwrap();
assert_eq!(snapshot.cpu.cores, 4);
assert_eq!(snapshot.memory.total_gb, 16.0);
}
}
```
## Configuration
```rust
// src/config.rs
use crate::error::{AgentError, Result};
use std::time::Duration;
#[derive(Debug, Clone)]
pub struct Config {
pub server_url: String,
pub timeout_secs: u64,
pub poll_interval: Duration,
pub agent_id: String,
}
impl Config {
pub fn from_env() -> Result<Self> {
let server_url = std::env::var("SERVER_URL")
.map_err(|_| AgentError::Config("SERVER_URL not set".to_string()))?;
let timeout_secs = std::env::var("TIMEOUT_SECS")
.unwrap_or_else(|_| "30".to_string())
.parse()
.map_err(|_| AgentError::Config("Invalid TIMEOUT_SECS".to_string()))?;
let poll_interval_secs: u64 = std::env::var("POLL_INTERVAL_SECS")
.unwrap_or_else(|_| "60".to_string())
.parse()
.map_err(|_| AgentError::Config("Invalid POLL_INTERVAL_SECS".to_string()))?;
let agent_id = std::env::var("AGENT_ID")
.unwrap_or_else(|_| hostname::get()
.map(|h| h.to_string_lossy().to_string())
.unwrap_or_else(|_| "unknown".to_string()));
Ok(Self {
server_url,
timeout_secs,
poll_interval: Duration::from_secs(poll_interval_secs),
agent_id,
})
}
}
```
## Clippy Configuration
```toml
# clippy.toml
cognitive-complexity-threshold = 10
too-many-arguments-threshold = 5
type-complexity-threshold = 200
```
## Commands
```bash
# Building
cargo build # Debug build
cargo build --release # Release build
cargo build --target x86_64-unknown-linux-musl # Static binary
# Testing
cargo test # Run all tests
cargo test -- --nocapture # Show println! output
cargo test test_name # Run specific test
cargo test --test integration # Run integration tests only
# Linting
cargo clippy # Run clippy
cargo clippy -- -D warnings # Treat warnings as errors
cargo clippy --fix # Auto-fix issues
# Formatting
cargo fmt # Format code
cargo fmt --check # Check formatting
# Other
cargo doc --open # Generate and open docs
cargo bench # Run benchmarks
cargo tree # Show dependency tree
cargo audit # Check for vulnerabilities
```
## Anti-Patterns to Avoid
```rust
// BAD: Unwrap without context
let config = Config::from_env().unwrap();
// GOOD: Provide context or use ?
let config = Config::from_env()
.expect("Failed to load configuration from environment");
// Or in functions returning Result:
let config = Config::from_env()?;
// BAD: Clone when not needed
fn process(data: &Vec<String>) {
let cloned = data.clone(); // Unnecessary allocation
for item in cloned.iter() {
println!("{}", item);
}
}
// GOOD: Use references
fn process(data: &[String]) {
for item in data {
println!("{}", item);
}
}
// BAD: String concatenation in loop
let mut result = String::new();
for item in items {
result = result + &item + ", "; // Allocates each iteration
}
// GOOD: Use push_str or join
let result = items.join(", ");
// BAD: Blocking in async context
async fn fetch_data() {
std::thread::sleep(Duration::from_secs(1)); // Blocks runtime!
}
// GOOD: Use async sleep
async fn fetch_data() {
tokio::time::sleep(Duration::from_secs(1)).await;
}
// BAD: Ignoring errors silently
let _ = file.write_all(data);
// GOOD: Handle or propagate errors
file.write_all(data)?;
// Or log if truly ignorable:
if let Err(e) = file.write_all(data) {
tracing::warn!(error = %e, "Failed to write data");
}
```

View File

@@ -0,0 +1,624 @@
---
name: typescript-react
description: TypeScript development with React, Vitest, ESLint, Zod, TanStack Query, and Radix UI patterns. Use when writing TypeScript, React components, or frontend code.
---
# TypeScript React Development Skill
## Project Structure
```
src/
├── main.tsx # App entry point
├── App.tsx # Root component
├── features/
│ └── users/
│ ├── components/
│ │ ├── UserList.tsx
│ │ └── UserForm.tsx
│ ├── hooks/
│ │ └── useUsers.ts
│ ├── api/
│ │ └── users.ts
│ └── schemas/
│ └── user.schema.ts
├── shared/
│ ├── components/
│ │ └── ui/ # Radix + Tailwind components
│ ├── hooks/
│ └── lib/
│ └── api-client.ts
└── test/
└── setup.ts # Vitest setup
tests/
└── features/
└── users/
└── UserList.test.tsx
```
## Vitest Configuration
```typescript
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./src/test/setup.ts'],
include: ['**/*.test.{ts,tsx}'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html', 'cobertura'],
exclude: [
'node_modules/',
'src/test/',
'**/*.d.ts',
'**/*.config.*',
'**/index.ts',
],
thresholds: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
},
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
});
```
### Test Setup
```typescript
// src/test/setup.ts
import '@testing-library/jest-dom/vitest';
import { cleanup } from '@testing-library/react';
import { afterEach, vi } from 'vitest';
// Cleanup after each test
afterEach(() => {
cleanup();
});
// Mock localStorage
const localStorageMock = {
getItem: vi.fn(),
setItem: vi.fn(),
removeItem: vi.fn(),
clear: vi.fn(),
};
Object.defineProperty(window, 'localStorage', { value: localStorageMock });
// Mock ResizeObserver (required for Radix UI)
class ResizeObserverMock {
observe = vi.fn();
unobserve = vi.fn();
disconnect = vi.fn();
}
window.ResizeObserver = ResizeObserverMock;
// Mock scrollIntoView
Element.prototype.scrollIntoView = vi.fn();
// Mock pointer capture (for Radix Select)
Element.prototype.hasPointerCapture = vi.fn();
Element.prototype.setPointerCapture = vi.fn();
Element.prototype.releasePointerCapture = vi.fn();
```
## Testing with React Testing Library
### Component Testing Pattern
```typescript
// tests/features/users/UserList.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect, vi } from 'vitest';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { UserList } from '@/features/users/components/UserList';
import { getMockUser } from './factories';
const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
},
});
return ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
};
describe('UserList', () => {
it('should display users when data loads', async () => {
const users = [
getMockUser({ name: 'Alice' }),
getMockUser({ name: 'Bob' }),
];
render(<UserList users={users} />, { wrapper: createWrapper() });
expect(screen.getByText('Alice')).toBeInTheDocument();
expect(screen.getByText('Bob')).toBeInTheDocument();
});
it('should show loading state initially', () => {
render(<UserList users={[]} isLoading />, { wrapper: createWrapper() });
expect(screen.getByRole('progressbar')).toBeInTheDocument();
});
it('should call onDelete when delete button clicked', async () => {
const user = userEvent.setup();
const onDelete = vi.fn();
const users = [getMockUser({ id: 'user-1', name: 'Alice' })];
render(<UserList users={users} onDelete={onDelete} />, {
wrapper: createWrapper(),
});
await user.click(screen.getByRole('button', { name: /delete alice/i }));
expect(onDelete).toHaveBeenCalledWith('user-1');
});
it('should filter users by search term', async () => {
const user = userEvent.setup();
const users = [
getMockUser({ name: 'Alice Smith' }),
getMockUser({ name: 'Bob Jones' }),
];
render(<UserList users={users} />, { wrapper: createWrapper() });
await user.type(screen.getByRole('searchbox'), 'Alice');
expect(screen.getByText('Alice Smith')).toBeInTheDocument();
expect(screen.queryByText('Bob Jones')).not.toBeInTheDocument();
});
});
```
### Factory Functions
```typescript
// tests/features/users/factories.ts
import type { User } from '@/features/users/schemas/user.schema';
export const getMockUser = (overrides?: Partial<User>): User => ({
id: 'user-123',
email: 'test@example.com',
name: 'Test User',
role: 'user',
createdAt: new Date().toISOString(),
...overrides,
});
export const getMockUserList = (count: number): User[] =>
Array.from({ length: count }, (_, i) =>
getMockUser({ id: `user-${i}`, name: `User ${i}` })
);
```
### Testing Hooks
```typescript
// tests/features/users/useUsers.test.tsx
import { renderHook, waitFor } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useUsers } from '@/features/users/hooks/useUsers';
import * as api from '@/features/users/api/users';
import { getMockUser } from './factories';
vi.mock('@/features/users/api/users');
const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
return ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
};
describe('useUsers', () => {
it('should return users on successful fetch', async () => {
const mockUsers = [getMockUser()];
vi.mocked(api.fetchUsers).mockResolvedValue(mockUsers);
const { result } = renderHook(() => useUsers(), {
wrapper: createWrapper(),
});
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(result.current.data).toEqual(mockUsers);
});
it('should return error state on failure', async () => {
vi.mocked(api.fetchUsers).mockRejectedValue(new Error('Network error'));
const { result } = renderHook(() => useUsers(), {
wrapper: createWrapper(),
});
await waitFor(() => expect(result.current.isError).toBe(true));
expect(result.current.error?.message).toBe('Network error');
});
});
```
## Zod Schema Patterns
### Schema Definition
```typescript
// src/features/users/schemas/user.schema.ts
import { z } from 'zod';
export const userSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
name: z.string().min(1).max(100),
role: z.enum(['admin', 'user', 'guest']),
createdAt: z.string().datetime(),
});
export type User = z.infer<typeof userSchema>;
export const userCreateSchema = userSchema.omit({ id: true, createdAt: true });
export type UserCreate = z.infer<typeof userCreateSchema>;
export const userUpdateSchema = userCreateSchema.partial();
export type UserUpdate = z.infer<typeof userUpdateSchema>;
// Validation at API boundary
export const parseUser = (data: unknown): User => userSchema.parse(data);
export const parseUsers = (data: unknown): User[] =>
z.array(userSchema).parse(data);
```
### Form Validation with React Hook Form
```typescript
// src/features/users/components/UserForm.tsx
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { userCreateSchema, type UserCreate } from '../schemas/user.schema';
import { Button } from '@/shared/components/ui/button';
import { Input } from '@/shared/components/ui/input';
type UserFormProps = {
onSubmit: (data: UserCreate) => void;
isLoading?: boolean;
};
export function UserForm({ onSubmit, isLoading }: UserFormProps) {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<UserCreate>({
resolver: zodResolver(userCreateSchema),
});
return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div>
<Input
{...register('email')}
type="email"
placeholder="Email"
aria-invalid={!!errors.email}
/>
{errors.email && (
<p className="text-sm text-red-500">{errors.email.message}</p>
)}
</div>
<div>
<Input
{...register('name')}
placeholder="Name"
aria-invalid={!!errors.name}
/>
{errors.name && (
<p className="text-sm text-red-500">{errors.name.message}</p>
)}
</div>
<Button type="submit" disabled={isLoading}>
{isLoading ? 'Creating...' : 'Create User'}
</Button>
</form>
);
}
```
## TanStack Query Patterns
### API Functions
```typescript
// src/features/users/api/users.ts
import { apiClient } from '@/shared/lib/api-client';
import { parseUsers, parseUser, type User, type UserCreate } from '../schemas/user.schema';
export async function fetchUsers(): Promise<User[]> {
const response = await apiClient.get('/users');
return parseUsers(response.data);
}
export async function fetchUser(id: string): Promise<User> {
const response = await apiClient.get(`/users/${id}`);
return parseUser(response.data);
}
export async function createUser(data: UserCreate): Promise<User> {
const response = await apiClient.post('/users', data);
return parseUser(response.data);
}
export async function deleteUser(id: string): Promise<void> {
await apiClient.delete(`/users/${id}`);
}
```
### Custom Hooks
```typescript
// src/features/users/hooks/useUsers.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { fetchUsers, createUser, deleteUser } from '../api/users';
import type { UserCreate } from '../schemas/user.schema';
export const userKeys = {
all: ['users'] as const,
lists: () => [...userKeys.all, 'list'] as const,
detail: (id: string) => [...userKeys.all, 'detail', id] as const,
};
export function useUsers() {
return useQuery({
queryKey: userKeys.lists(),
queryFn: fetchUsers,
});
}
export function useCreateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: UserCreate) => createUser(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: userKeys.lists() });
},
});
}
export function useDeleteUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => deleteUser(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: userKeys.lists() });
},
});
}
```
## ESLint Configuration
```javascript
// eslint.config.js
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh';
export default tseslint.config(
{ ignores: ['dist', 'coverage', 'node_modules'] },
{
extends: [js.configs.recommended, ...tseslint.configs.strictTypeChecked],
files: ['**/*.{ts,tsx}'],
languageOptions: {
parserOptions: {
project: ['./tsconfig.json', './tsconfig.node.json'],
tsconfigRootDir: import.meta.dirname,
},
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
'@typescript-eslint/no-unused-vars': [
'error',
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
],
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/consistent-type-imports': [
'error',
{ prefer: 'type-imports' },
],
},
}
);
```
## TypeScript Configuration
```json
// tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src", "tests"]
}
```
## Component Patterns
### Accessible Component with Radix
```typescript
// src/shared/components/ui/button.tsx
import * as React from 'react';
import { Slot } from '@radix-ui/react-slot';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/shared/lib/utils';
const buttonVariants = cva(
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
outline: 'border border-input hover:bg-accent hover:text-accent-foreground',
ghost: 'hover:bg-accent hover:text-accent-foreground',
},
size: {
default: 'h-10 px-4 py-2',
sm: 'h-9 rounded-md px-3',
lg: 'h-11 rounded-md px-8',
icon: 'h-10 w-10',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
}
);
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean;
};
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : 'button';
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
}
);
Button.displayName = 'Button';
```
## Commands
```bash
# Testing
npm test # Run tests once
npm run test:watch # Watch mode
npm run test:coverage # With coverage
npm run test:ui # Vitest UI
# Linting & Type Checking
npm run lint # ESLint
npm run lint:fix # Auto-fix
npm run typecheck # tsc --noEmit
# Development
npm run dev # Vite dev server
npm run build # Production build
npm run preview # Preview build
```
## Anti-Patterns to Avoid
```typescript
// BAD: any type
const processData = (data: any) => data.value;
// GOOD: proper typing or unknown
const processData = (data: unknown): string => {
const parsed = schema.parse(data);
return parsed.value;
};
// BAD: Mutation in state update
const addItem = (item: Item) => {
items.push(item); // Mutates array!
setItems(items);
};
// GOOD: Immutable update
const addItem = (item: Item) => {
setItems((prev) => [...prev, item]);
};
// BAD: useEffect for derived state
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
// GOOD: Derive directly
const fullName = `${firstName} ${lastName}`;
// BAD: Testing implementation details
it('should call setState', () => {
const spy = vi.spyOn(React, 'useState');
render(<Component />);
expect(spy).toHaveBeenCalled();
});
// GOOD: Test user-visible behavior
it('should show error message for invalid input', async () => {
render(<Form />);
await userEvent.type(screen.getByLabelText('Email'), 'invalid');
await userEvent.click(screen.getByRole('button', { name: 'Submit' }));
expect(screen.getByText('Invalid email')).toBeInTheDocument();
});
```