Bindings Best Practices
Bindings Best Practices
Section titled “Bindings Best Practices”Follow proven patterns for binding design to create clean, performant, and secure bindings. This guide covers API design principles, performance optimisation, security patterns, error handling, and testing strategies for maintainable applications.
API Design Principles
Section titled “API Design Principles”1. Single Responsibility
Section titled “1. Single Responsibility”Each service should have one clear purpose:
// ❌ Bad: God objecttype AppService struct { // Does everything}
func (a *AppService) SaveFile(path string, data []byte) errorfunc (a *AppService) GetUser(id int) (*User, error)func (a *AppService) SendEmail(to, subject, body string) errorfunc (a *AppService) ProcessPayment(amount float64) error
// ✅ Good: Focused servicestype FileService struct{}func (f *FileService) Save(path string, data []byte) error
type UserService struct{}func (u *UserService) GetByID(id int) (*User, error)
type EmailService struct{}func (e *EmailService) Send(to, subject, body string) error
type PaymentService struct{}func (p *PaymentService) Process(amount float64) error2. Clear Method Names
Section titled “2. Clear Method Names”Use descriptive, action-oriented names:
// ❌ Bad: Unclear namesfunc (s *Service) Do(x string) errorfunc (s *Service) Handle(data interface{}) interface{}func (s *Service) Process(input map[string]interface{}) bool
// ✅ Good: Clear namesfunc (s *FileService) SaveDocument(path string, content string) errorfunc (s *UserService) AuthenticateUser(email, password string) (*User, error)func (s *OrderService) CreateOrder(items []Item) (*Order, error)3. Consistent Return Types
Section titled “3. Consistent Return Types”Always return errors explicitly:
// ❌ Bad: Inconsistent error handlingfunc (s *Service) GetData() interface{} // How to handle errors?func (s *Service) SaveData(data string) // Silent failures?
// ✅ Good: Explicit errorsfunc (s *Service) GetData() (Data, error)func (s *Service) SaveData(data string) error4. Input Validation
Section titled “4. Input Validation”Validate all input on the Go side:
// ❌ Bad: No validationfunc (s *UserService) CreateUser(email, password string) (*User, error) { user := &User{Email: email, Password: password} return s.db.Create(user)}
// ✅ Good: Validate firstfunc (s *UserService) CreateUser(email, password string) (*User, error) { // Validate email if !isValidEmail(email) { return nil, errors.New("invalid email address") }
// Validate password if len(password) < 8 { return nil, errors.New("password must be at least 8 characters") }
// Hash password hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { return nil, err }
user := &User{ Email: email, PasswordHash: string(hash), }
return s.db.Create(user)}Performance Patterns
Section titled “Performance Patterns”1. Batch Operations
Section titled “1. Batch Operations”Reduce bridge calls by batching:
// ❌ Bad: N calls// JavaScriptfor (const item of items) { await ProcessItem(item) // N bridge calls}
// ✅ Good: 1 call// Gofunc (s *Service) ProcessItems(items []Item) ([]Result, error) { results := make([]Result, len(items)) for i, item := range items { results[i] = s.processItem(item) } return results, nil}
// JavaScriptconst results = await ProcessItems(items) // 1 bridge call2. Pagination
Section titled “2. Pagination”Don’t return huge datasets:
// ❌ Bad: Returns everythingfunc (s *Service) GetAllUsers() ([]User, error) { return s.db.FindAll() // Could be millions}
// ✅ Good: Paginatedtype PageRequest struct { Page int `json:"page"` PageSize int `json:"pageSize"`}
type PageResponse struct { Items []User `json:"items"` TotalItems int `json:"totalItems"` TotalPages int `json:"totalPages"` Page int `json:"page"`}
func (s *Service) GetUsers(req PageRequest) (*PageResponse, error) { // Validate if req.Page < 1 { req.Page = 1 } if req.PageSize < 1 || req.PageSize > 100 { req.PageSize = 20 }
// Get total total, err := s.db.Count() if err != nil { return nil, err }
// Get page offset := (req.Page - 1) * req.PageSize users, err := s.db.Find(offset, req.PageSize) if err != nil { return nil, err }
return &PageResponse{ Items: users, TotalItems: total, TotalPages: (total + req.PageSize - 1) / req.PageSize, Page: req.Page, }, nil}3. Caching
Section titled “3. Caching”Cache expensive operations:
type CachedService struct { cache map[string]interface{} mu sync.RWMutex ttl time.Duration}
func (s *CachedService) GetData(key string) (interface{}, error) { // Check cache s.mu.RLock() if data, ok := s.cache[key]; ok { s.mu.RUnlock() return data, nil } s.mu.RUnlock()
// Fetch data data, err := s.fetchData(key) if err != nil { return nil, err }
// Cache it s.mu.Lock() s.cache[key] = data s.mu.Unlock()
// Schedule expiry go func() { time.Sleep(s.ttl) s.mu.Lock() delete(s.cache, key) s.mu.Unlock() }()
return data, nil}4. Streaming with Events
Section titled “4. Streaming with Events”Use events for streaming data:
// ❌ Bad: Pollingfunc (s *Service) GetProgress() int { return s.progress}
// JavaScript pollssetInterval(async () => { const progress = await GetProgress() updateUI(progress)}, 100)
// ✅ Good: Eventsfunc (s *Service) ProcessLargeFile(path string) error { file, err := os.Open(path) if err != nil { return err } defer file.Close()
scanner := bufio.NewScanner(file) total := 0 processed := 0
// Count lines for scanner.Scan() { total++ }
// Process file.Seek(0, 0) scanner = bufio.NewScanner(file)
for scanner.Scan() { s.processLine(scanner.Text()) processed++
// Emit progress s.app.Event.Emit("progress", map[string]interface{}{ "processed": processed, "total": total, "percent": int(float64(processed) / float64(total) * 100), }) }
return scanner.Err()}
// JavaScript listensOnEvent("progress", (data) => { updateProgress(data.percent)})Security Patterns
Section titled “Security Patterns”1. Input Sanitisation
Section titled “1. Input Sanitisation”Always sanitise user input:
import ( "html" "strings")
func (s *Service) SaveComment(text string) error { // Sanitise text = strings.TrimSpace(text) text = html.EscapeString(text)
// Validate length if len(text) == 0 { return errors.New("comment cannot be empty") } if len(text) > 1000 { return errors.New("comment too long") }
return s.db.SaveComment(text)}2. Authentication
Section titled “2. Authentication”Protect sensitive operations:
type AuthService struct { sessions map[string]*Session mu sync.RWMutex}
func (a *AuthService) Login(email, password string) (string, error) { user, err := a.db.FindByEmail(email) if err != nil { return "", errors.New("invalid credentials") }
if !a.verifyPassword(user.PasswordHash, password) { return "", errors.New("invalid credentials") }
// Create session token := generateToken() a.mu.Lock() a.sessions[token] = &Session{ UserID: user.ID, ExpiresAt: time.Now().Add(24 * time.Hour), } a.mu.Unlock()
return token, nil}
func (a *AuthService) requireAuth(token string) (*Session, error) { a.mu.RLock() session, ok := a.sessions[token] a.mu.RUnlock()
if !ok { return nil, errors.New("not authenticated") }
if time.Now().After(session.ExpiresAt) { return nil, errors.New("session expired") }
return session, nil}
// Protected methodfunc (a *AuthService) DeleteAccount(token string) error { session, err := a.requireAuth(token) if err != nil { return err }
return a.db.DeleteUser(session.UserID)}3. Rate Limiting
Section titled “3. Rate Limiting”Prevent abuse:
type RateLimiter struct { requests map[string][]time.Time mu sync.Mutex limit int window time.Duration}
func (r *RateLimiter) Allow(key string) bool { r.mu.Lock() defer r.mu.Unlock()
now := time.Now()
// Clean old requests if requests, ok := r.requests[key]; ok { var recent []time.Time for _, t := range requests { if now.Sub(t) < r.window { recent = append(recent, t) } } r.requests[key] = recent }
// Check limit if len(r.requests[key]) >= r.limit { return false }
// Add request r.requests[key] = append(r.requests[key], now) return true}
// Usagefunc (s *Service) SendEmail(to, subject, body string) error { if !s.rateLimiter.Allow(to) { return errors.New("rate limit exceeded") }
return s.emailer.Send(to, subject, body)}Error Handling Patterns
Section titled “Error Handling Patterns”1. Descriptive Errors
Section titled “1. Descriptive Errors”Provide context in errors:
// ❌ Bad: Generic errorsfunc (s *Service) LoadFile(path string) ([]byte, error) { return os.ReadFile(path) // "no such file or directory"}
// ✅ Good: Contextual errorsfunc (s *Service) LoadFile(path string) ([]byte, error) { data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("failed to load file %s: %w", path, err) } return data, nil}2. Error Types
Section titled “2. Error Types”Use typed errors for specific handling:
type ValidationError struct { Field string Message string}
func (e *ValidationError) Error() string { return fmt.Sprintf("%s: %s", e.Field, e.Message)}
type NotFoundError struct { Resource string ID interface{}}
func (e *NotFoundError) Error() string { return fmt.Sprintf("%s not found: %v", e.Resource, e.ID)}
// Usagefunc (s *UserService) GetUser(id int) (*User, error) { if id <= 0 { return nil, &ValidationError{ Field: "id", Message: "must be positive", } }
user, err := s.db.Find(id) if err == sql.ErrNoRows { return nil, &NotFoundError{ Resource: "User", ID: id, } } if err != nil { return nil, fmt.Errorf("database error: %w", err) }
return user, nil}3. Error Recovery
Section titled “3. Error Recovery”Handle errors gracefully:
func (s *Service) ProcessWithRetry(data string) error { maxRetries := 3
for attempt := 1; attempt <= maxRetries; attempt++ { err := s.process(data) if err == nil { return nil }
// Log attempt s.app.Logger.Warn("Process failed", "attempt", attempt, "error", err)
// Don't retry on validation errors if _, ok := err.(*ValidationError); ok { return err }
// Wait before retry if attempt < maxRetries { time.Sleep(time.Duration(attempt) * time.Second) } }
return fmt.Errorf("failed after %d attempts", maxRetries)}Testing Patterns
Section titled “Testing Patterns”1. Unit Testing
Section titled “1. Unit Testing”Test services in isolation:
func TestUserService_CreateUser(t *testing.T) { // Setup db := &MockDB{} service := &UserService{db: db}
// Test valid input user, err := service.CreateUser("test@example.com", "password123") if err != nil { t.Fatalf("unexpected error: %v", err) } if user.Email != "test@example.com" { t.Errorf("expected email test@example.com, got %s", user.Email) }
// Test invalid email _, err = service.CreateUser("invalid", "password123") if err == nil { t.Error("expected error for invalid email") }
// Test short password _, err = service.CreateUser("test@example.com", "short") if err == nil { t.Error("expected error for short password") }}2. Integration Testing
Section titled “2. Integration Testing”Test with real dependencies:
func TestUserService_Integration(t *testing.T) { // Setup real database db, err := sql.Open("sqlite3", ":memory:") if err != nil { t.Fatal(err) } defer db.Close()
// Create schema _, err = db.Exec(`CREATE TABLE users (...)`) if err != nil { t.Fatal(err) }
// Test service service := &UserService{db: db}
user, err := service.CreateUser("test@example.com", "password123") if err != nil { t.Fatal(err) }
// Verify in database var count int db.QueryRow("SELECT COUNT(*) FROM users WHERE email = ?", user.Email).Scan(&count)
if count != 1 { t.Errorf("expected 1 user, got %d", count) }}3. Mock Services
Section titled “3. Mock Services”Create testable interfaces:
type UserRepository interface { Create(user *User) error FindByEmail(email string) (*User, error) Update(user *User) error Delete(id int) error}
type UserService struct { repo UserRepository}
// Mock for testingtype MockUserRepository struct { users map[string]*User}
func (m *MockUserRepository) Create(user *User) error { m.users[user.Email] = user return nil}
// Test with mockfunc TestUserService_WithMock(t *testing.T) { mock := &MockUserRepository{ users: make(map[string]*User), }
service := &UserService{repo: mock}
// Test user, err := service.CreateUser("test@example.com", "password123") if err != nil { t.Fatal(err) }
// Verify mock was called if len(mock.users) != 1 { t.Error("expected 1 user in mock") }}Best Practices Summary
Section titled “Best Practices Summary”- Single responsibility - One service, one purpose
- Clear naming - Descriptive method names
- Validate input - Always on Go side
- Return errors - Explicit error handling
- Batch operations - Reduce bridge calls
- Use events - For streaming data
- Sanitise input - Prevent injection
- Test thoroughly - Unit and integration tests
- Document methods - Comments become JSDoc
- Version your API - Plan for changes
❌ Don’t
Section titled “❌ Don’t”- Don’t create god objects - Keep services focused
- Don’t trust frontend - Validate everything
- Don’t return huge datasets - Use pagination
- Don’t block - Use goroutines for long operations
- Don’t ignore errors - Handle all error cases
- Don’t skip testing - Test early and often
- Don’t hardcode - Use configuration
- Don’t expose internals - Keep implementation private
Next Steps
Section titled “Next Steps”- Methods - Learn method binding basics
- Services - Understand service architecture
- Models - Bind complex data structures
- Events - Use events for pub/sub
Questions? Ask in Discord or check the binding examples.