Best Practices
Best Practices
Section titled “Best Practices”This guide provides best practices and design patterns for developing MongoDB applications with Pie.
Project Structure
Section titled “Project Structure”Recommended Directory Structure
Section titled “Recommended Directory Structure”project/├── cmd/│ └── server/│ └── main.go├── internal/│ ├── config/│ │ └── config.go│ ├── database/│ │ └── connection.go│ ├── models/│ │ ├── user.go│ │ ├── order.go│ │ └── product.go│ ├── repositories/│ │ ├── user_repository.go│ │ ├── order_repository.go│ │ └── product_repository.go│ ├── services/│ │ ├── user_service.go│ │ ├── order_service.go│ │ └── product_service.go│ └── handlers/│ ├── user_handler.go│ ├── order_handler.go│ └── product_handler.go├── pkg/│ └── utils/│ └── validation.go├── migrations/│ └── indexes.go├── tests/│ ├── integration/│ └── unit/└── go.modModel Design
Section titled “Model Design”Base Model
Section titled “Base Model”package models
import ( "time" "go.mongodb.org/mongo-driver/v2/bson")
type BaseModel struct { ID bson.ObjectID `bson:"_id,omitempty" json:"id"` CreatedAt time.Time `bson:"created_at" json:"created_at"` UpdatedAt time.Time `bson:"updated_at" json:"updated_at"` DeletedAt *time.Time `bson:"deleted_at,omitempty" json:"deleted_at,omitempty" pie:"soft_delete"`}
func (m *BaseModel) BeforeCreate(ctx context.Context) error { now := time.Now() m.CreatedAt = now m.UpdatedAt = now return nil}
func (m *BaseModel) BeforeUpdate(ctx context.Context) error { m.UpdatedAt = time.Now() return nil}Repository Pattern
Section titled “Repository Pattern”Base Repository
Section titled “Base Repository”package repositories
import ( "context" "github.com/5xxxx/pie")
type BaseRepository[T any] struct { engine *pie.Engine session *pie.Session[T]}
func NewBaseRepository[T any](engine *pie.Engine) *BaseRepository[T] { return &BaseRepository[T]{ engine: engine, session: pie.Table[T](engine), }}
func (r *BaseRepository[T]) Create(ctx context.Context, entity *T) error { _, err := r.session.Insert(ctx, entity) return err}
func (r *BaseRepository[T]) GetByID(ctx context.Context, id bson.ObjectID) (*T, error) { entity, err := r.session.Where("_id", id).FindOne(ctx) if err != nil { return nil, err } return entity, nil}Error Handling
Section titled “Error Handling”Custom Error Types
Section titled “Custom Error Types”package errors
import "fmt"
type AppError struct { Code string Message string Err error}
func (e *AppError) Error() string { if e.Err != nil { return fmt.Sprintf("%s: %v", e.Message, e.Err) } return e.Message}
func (e *AppError) Unwrap() error { return e.Err}
// Predefined errorsvar ( ErrUserNotFound = &AppError{Code: "USER_NOT_FOUND", Message: "user not found"} ErrEmailExists = &AppError{Code: "EMAIL_EXISTS", Message: "email already exists"} ErrInvalidPassword = &AppError{Code: "INVALID_PASSWORD", Message: "invalid password"} ErrUnauthorized = &AppError{Code: "UNAUTHORIZED", Message: "unauthorized"} ErrForbidden = &AppError{Code: "FORBIDDEN", Message: "forbidden"})Testing Strategy
Section titled “Testing Strategy”Unit Tests
Section titled “Unit Tests”package unit
import ( "context" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "your-project/internal/services")
type MockUserRepository struct { mock.Mock}
func (m *MockUserRepository) GetByEmail(ctx context.Context, email string) (*models.User, error) { args := m.Called(ctx, email) return args.Get(0).(*models.User), args.Error(1)}
func TestUserService_CreateUser(t *testing.T) { mockRepo := new(MockUserRepository) service := &services.UserService{UserRepo: mockRepo}
// Test case: Success t.Run("Success", func(t *testing.T) { mockRepo.On("GetByEmail", mock.Anything, "test@example.com").Return((*models.User)(nil), errors.New("not found")) mockRepo.On("Create", mock.Anything, mock.AnythingOfType("*models.User")).Return(nil)
userData := &services.CreateUserRequest{ Name: "Test User", Email: "test@example.com", Password: "password123", Role: "user", }
user, err := service.CreateUser(context.Background(), userData)
assert.NoError(t, err) assert.NotNil(t, user) assert.Equal(t, "Test User", user.Name) assert.Equal(t, "test@example.com", user.Email)
mockRepo.AssertExpectations(t) })}Performance Optimization
Section titled “Performance Optimization”Connection Pool Configuration
Section titled “Connection Pool Configuration”// Optimize connection poolengine, err := pie.NewEngine(ctx, "mydb", pie.WithMaxPoolSize(100), // Max connections pie.WithMinPoolSize(10), // Min connections pie.WithMaxIdleTime(30*time.Minute), // Max idle time)Caching Strategy
Section titled “Caching Strategy”// Multi-level cachingengine.UseTwoLevelCache( pie.NewMemoryCache(), // L1 cache redisCache, // L2 cache &pie.TwoLevelCacheConfig{ L1TTL: 1 * time.Minute, // L1 cache 1 minute L2TTL: 10 * time.Minute, // L2 cache 10 minutes },)Security Best Practices
Section titled “Security Best Practices”Input Validation
Section titled “Input Validation”package validation
import ( "regexp" "strings" "unicode")
func ValidateEmail(email string) bool { pattern := `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$` matched, _ := regexp.MatchString(pattern, email) return matched}
func ValidatePassword(password string) error { if len(password) < 8 { return errors.New("password must be at least 8 characters long") }
var hasUpper, hasLower, hasNumber, hasSpecial bool for _, char := range password { switch { case unicode.IsUpper(char): hasUpper = true case unicode.IsLower(char): hasLower = true case unicode.IsNumber(char): hasNumber = true case unicode.IsPunct(char) || unicode.IsSymbol(char): hasSpecial = true } }
if !hasUpper { return errors.New("password must contain at least one uppercase letter") } if !hasLower { return errors.New("password must contain at least one lowercase letter") } if !hasNumber { return errors.New("password must contain at least one number") } if !hasSpecial { return errors.New("password must contain at least one special character") }
return nil}Summary
Section titled “Summary”Following these best practices will help you build maintainable, scalable, and high-performance MongoDB applications:
- Project Structure: Use clear layered architecture
- Model Design: Use hooks and validation appropriately
- Repository Pattern: Abstract data access layer
- Error Handling: Unified error handling mechanism
- Testing Strategy: Comprehensive unit and integration tests
- Performance: Connection pooling and caching
- Security: Input validation and access control
These practices will help you make the most of Pie’s powerful features and build high-quality applications.