diff --git a/docs/src/content/docs/guides/patterns/_category_.json b/docs/src/content/docs/guides/patterns/_category_.json new file mode 100644 index 00000000000..a1cbc077354 --- /dev/null +++ b/docs/src/content/docs/guides/patterns/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Patterns", + "position": 1, + "link": { + "type": "generated-index", + "description": "Common patterns and architectures for Wails applications" + } +} diff --git a/docs/src/content/docs/guides/patterns/database.mdx b/docs/src/content/docs/guides/patterns/database.mdx new file mode 100644 index 00000000000..b2c2a1da0b3 --- /dev/null +++ b/docs/src/content/docs/guides/patterns/database.mdx @@ -0,0 +1,337 @@ +--- +title: Database Integration +description: Best practices for integrating databases with Wails v3 applications +--- + +import { Tabs, TabItem } from "@astrojs/starlight/components"; + +Database integration is essential for most desktop applications. This guide demonstrates how to work with popular database solutions in Wails v3. + +## Overview + +Wails v3 provides flexible options for database integration. You can use: + +- **SQL Databases:** SQLite, PostgreSQL, MySQL (via drivers) +- **ORMs:** GORM, sqlc, or raw database/sql +- **Document Stores:** MongoDB drivers +- **Embedded Databases:** SQLite, BadgerDB, BoltDB + +## Using SQLite with GORM + +SQLite is an excellent choice for desktop applications due to its embedded nature and simplicity. + +### 1. Install Dependencies + +```bash +go get -u gorm.io/gorm +go get -u gorm.io/driver/sqlite +``` + +### 2. Define Your Models + +```go +package models + +import "gorm.io/gorm" + +type User struct { + ID uint `gorm:"primaryKey"` + Name string + Email string + Phone string +} + +type Product struct { + ID uint + Name string + Price float64 + Stock int +} +``` + +### 3. Initialize the Database + +```go +package database + +import ( + "github.com/yourapp/models" + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +var DB *gorm.DB + +func InitDB(dbPath string) error { + var err error + DB, err = gorm.Open(sqlite.Open(dbPath), &gorm.Config{}) + if err != nil { + return err + } + + // Auto-migrate models + return DB.AutoMigrate(&models.User{}, &models.Product{}) +} +``` + +### 4. Use in Your Application + +```go +package main + +import ( + "github.com/yourapp/database" + "github.com/wailsapp/wails/v3/pkg/application" +) + +func main() { + // Initialize database + if err := database.InitDB("app.db"); err != nil { + panic(err) + } + + app := application.New(application.Options{ + Name: "My App", + Description: "Database-backed application", + }) + + // Create window + app.Window.NewWithOptions(application.WebviewWindowOptions{ + Title: "My App", + URL: "/", + Width: 800, + Height: 600, + }) + + if err := app.Run(); err != nil { + panic(err) + } +} +``` + +## Creating Database Services + +Encapsulate database operations in services for better organization and testability. + +### User Service Example + +```go +package services + +import ( + "github.com/yourapp/database" + "github.com/yourapp/models" + "gorm.io/gorm" +) + +type UserService struct{} + +// GetAllUsers returns all users +func (s *UserService) GetAllUsers() ([]models.User, error) { + var users []models.User + if err := database.DB.Find(&users).Error; err != nil { + return nil, err + } + return users, nil +} + +// GetUser returns a single user by ID +func (s *UserService) GetUser(id uint) (*models.User, error) { + var user models.User + if err := database.DB.First(&user, id).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return nil, nil + } + return nil, err + } + return &user, nil +} + +// CreateUser creates a new user +func (s *UserService) CreateUser(name, email, phone string) (*models.User, error) { + user := models.User{ + Name: name, + Email: email, + Phone: phone, + } + if err := database.DB.Create(&user).Error; err != nil { + return nil, err + } + return &user, nil +} + +// UpdateUser updates an existing user +func (s *UserService) UpdateUser(id uint, name, email, phone string) (*models.User, error) { + user := models.User{ID: id} + if err := database.DB.Model(&user).Updates(models.User{ + Name: name, + Email: email, + Phone: phone, + }).Error; err != nil { + return nil, err + } + return &user, nil +} + +// DeleteUser deletes a user +func (s *UserService) DeleteUser(id uint) error { + return database.DB.Delete(&models.User{}, id).Error +} +``` + +## Exposing Database Operations to Frontend + +Use Wails binding system to expose database services to your frontend: + +```go +package main + +import ( + "github.com/yourapp/services" + "github.com/wailsapp/wails/v3/pkg/application" +) + +func main() { + userService := &services.UserService{} + + app := application.New(application.Options{ + Name: "My App", + Services: []application.Service{ + application.NewService(userService), + }, + }) + + // ... rest of application setup +} +``` + +Call from frontend: + + + +```typescript +import { UserService } from './services/user-service'; + +// Get all users +const users = await UserService.GetAllUsers(); + +// Create a user +const newUser = await UserService.CreateUser('John Doe', 'john@example.com', '123-456-7890'); + +// Update a user +const updated = await UserService.UpdateUser(1, 'Jane Doe', 'jane@example.com', '098-765-4321'); + +// Delete a user +await UserService.DeleteUser(1); +``` + + +```javascript +// Get all users +const users = await window.go.services.UserService.GetAllUsers(); + +// Create a user +const newUser = await window.go.services.UserService.CreateUser( + 'John Doe', + 'john@example.com', + '123-456-7890' +); + +// Update a user +const updated = await window.go.services.UserService.UpdateUser( + 1, + 'Jane Doe', + 'jane@example.com', + '098-765-4321' +); + +// Delete a user +await window.go.services.UserService.DeleteUser(1); +``` + + + +## Migration Management + +Use GORM migrations or a dedicated migration tool for schema management. + +### Automatic Migration + +GORM's `AutoMigrate` handles schema updates automatically: + +```go +db.AutoMigrate(&models.User{}, &models.Product{}) +``` + +### Manual Migration Control + +For more control, create explicit migrations: + +```go +type User struct { + ID uint + Name string + Email string `gorm:"uniqueIndex"` + CreatedAt time.Time + UpdatedAt time.Time +} + +// Run migrations with hooks +if err := db.Migrator().CreateTable(&User{}); err != nil { + return err +} + +if err := db.Migrator().CreateIndex(&User{}, "email"); err != nil { + return err +} +``` + +## Transactions + +Use transactions for operations that require atomicity: + +```go +// Begin transaction +tx := database.DB.BeginTx(ctx, nil) + +// Perform operations +if err := tx.Create(&user1).Error; err != nil { + tx.Rollback() + return err +} + +if err := tx.Create(&user2).Error; err != nil { + tx.Rollback() + return err +} + +// Commit if all successful +if err := tx.Commit().Error; err != nil { + return err +} +``` + +## Best Practices + +- **Separate Concerns:** Keep database logic in service layers +- **Handle Errors:** Always check and handle database errors +- **Use Transactions:** For multi-operation workflows +- **Index Strategically:** Create indexes on frequently queried columns +- **Connection Pooling:** Configure connection limits for networked databases +- **Backup Regularly:** Implement backup strategies for important data +- **Encrypt Sensitive Data:** Use encryption for passwords and sensitive information +- **Validate Input:** Always validate and sanitize user input before database operations + +## Performance Considerations + +- Use GORM's eager loading to avoid N+1 queries +- Create appropriate indexes for commonly filtered columns +- Use pagination for large result sets +- Consider denormalization for read-heavy workloads +- Profile your queries to identify bottlenecks + +## Additional Resources + +- [GORM Documentation](https://gorm.io/) +- [SQLite Documentation](https://www.sqlite.org/) +- [Database/SQL Package](https://pkg.go.dev/database/sql) diff --git a/docs/src/content/docs/guides/gin-routing.mdx b/docs/src/content/docs/guides/patterns/gin-routing.mdx similarity index 100% rename from docs/src/content/docs/guides/gin-routing.mdx rename to docs/src/content/docs/guides/patterns/gin-routing.mdx diff --git a/docs/src/content/docs/guides/gin-services.mdx b/docs/src/content/docs/guides/patterns/gin-services.mdx similarity index 100% rename from docs/src/content/docs/guides/gin-services.mdx rename to docs/src/content/docs/guides/patterns/gin-services.mdx diff --git a/docs/src/content/docs/guides/patterns/rest-api.mdx b/docs/src/content/docs/guides/patterns/rest-api.mdx new file mode 100644 index 00000000000..ba7d6ec1db1 --- /dev/null +++ b/docs/src/content/docs/guides/patterns/rest-api.mdx @@ -0,0 +1,496 @@ +--- +title: Building REST APIs +description: Build robust REST APIs with Wails v3 using best practices and common patterns +--- + +import { Card, CardGrid, Tabs, TabItem } from "@astrojs/starlight/components"; + +This guide demonstrates best practices for building REST APIs in Wails v3 applications. Whether you're building a simple HTTP interface or a comprehensive API, these patterns will help you create maintainable and scalable solutions. + +## Overview + +Wails v3 provides multiple approaches for building REST APIs: + +1. **Direct HTTP Binding** - Expose Go methods directly to frontend +2. **Service with Router** - Use a framework like Gin for complex APIs +3. **HTTP Middleware** - Custom request/response handling +4. **REST Service Pattern** - Organized REST endpoints with proper status codes + +## Direct HTTP Binding + +The simplest approach for basic CRUD operations. + +### Define Your Types + +```go +package models + +type User struct { + ID int `json:"id"` + Name string `json:"name"` + Email string `json:"email"` +} + +type CreateUserRequest struct { + Name string `json:"name"` + Email string `json:"email"` +} +``` + +### Create Your API Service + +```go +package api + +import ( + "fmt" + "sync" +) + +type UserAPI struct { + users map[int]User + nextID int + mu sync.RWMutex +} + +func NewUserAPI() *UserAPI { + return &UserAPI{ + users: make(map[int]User), + nextID: 1, + } +} + +// GetUsers returns all users +func (api *UserAPI) GetUsers() []User { + api.mu.RLock() + defer api.mu.RUnlock() + + result := make([]User, 0) + for _, user := range api.users { + result = append(result, user) + } + return result +} + +// GetUser returns a single user +func (api *UserAPI) GetUser(id int) (*User, error) { + api.mu.RLock() + defer api.mu.RUnlock() + + user, ok := api.users[id] + if !ok { + return nil, fmt.Errorf("user not found") + } + return &user, nil +} + +// CreateUser creates a new user +func (api *UserAPI) CreateUser(name, email string) (*User, error) { + if name == "" || email == "" { + return nil, fmt.Errorf("name and email required") + } + + api.mu.Lock() + defer api.mu.Unlock() + + user := User{ + ID: api.nextID, + Name: name, + Email: email, + } + api.users[api.nextID] = user + api.nextID++ + + return &user, nil +} + +// UpdateUser updates an existing user +func (api *UserAPI) UpdateUser(id int, name, email string) (*User, error) { + api.mu.Lock() + defer api.mu.Unlock() + + user, ok := api.users[id] + if !ok { + return nil, fmt.Errorf("user not found") + } + + if name != "" { + user.Name = name + } + if email != "" { + user.Email = email + } + + api.users[id] = user + return &user, nil +} + +// DeleteUser deletes a user +func (api *UserAPI) DeleteUser(id int) error { + api.mu.Lock() + defer api.mu.Unlock() + + if _, ok := api.users[id]; !ok { + return fmt.Errorf("user not found") + } + delete(api.users, id) + return nil +} +``` + +### Register in Wails Application + +```go +package main + +import ( + "github.com/yourapp/api" + "github.com/wailsapp/wails/v3/pkg/application" +) + +func main() { + userAPI := api.NewUserAPI() + + app := application.New(application.Options{ + Name: "API Example", + Services: []application.Service{ + application.NewService(userAPI), + }, + }) + + app.Window.NewWithOptions(application.WebviewWindowOptions{ + Title: "API Example", + URL: "/", + }) + + if err := app.Run(); err != nil { + panic(err) + } +} +``` + +### Frontend Usage + + + +```typescript +import { UserAPI } from './services/user-api'; + +// Get all users +const users = await UserAPI.GetUsers(); + +// Get a single user +try { + const user = await UserAPI.GetUser(1); +} catch (error) { + console.error('User not found:', error); +} + +// Create a user +const newUser = await UserAPI.CreateUser('John Doe', 'john@example.com'); + +// Update a user +const updated = await UserAPI.UpdateUser(1, 'Jane Doe', 'jane@example.com'); + +// Delete a user +try { + await UserAPI.DeleteUser(1); +} catch (error) { + console.error('Failed to delete:', error); +} +``` + + +```javascript +// Get all users +const users = await window.go.api.UserAPI.GetUsers(); + +// Get a single user +try { + const user = await window.go.api.UserAPI.GetUser(1); +} catch (error) { + console.error('User not found:', error); +} + +// Create a user +const newUser = await window.go.api.UserAPI.CreateUser( + 'John Doe', + 'john@example.com' +); + +// Update a user +const updated = await window.go.api.UserAPI.UpdateUser( + 1, + 'Jane Doe', + 'jane@example.com' +); + +// Delete a user +try { + await window.go.api.UserAPI.DeleteUser(1); +} catch (error) { + console.error('Failed to delete:', error); +} +``` + + + +## REST API with Gin Framework + +For more complex APIs, use a dedicated framework like Gin. + +### Set Up Routes + +```go +package api + +import ( + "net/http" + "strconv" + + "github.com/gin-gonic/gin" +) + +func SetupRoutes(router *gin.Engine, api *UserAPI) { + users := router.Group("/users") + { + users.GET("", func(c *gin.Context) { + users := api.GetUsers() + c.JSON(http.StatusOK, users) + }) + + users.GET("/:id", func(c *gin.Context) { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"}) + return + } + + user, err := api.GetUser(id) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, user) + }) + + users.POST("", func(c *gin.Context) { + var req struct { + Name string `json:"name" binding:"required"` + Email string `json:"email" binding:"required,email"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + user, err := api.CreateUser(req.Name, req.Email) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusCreated, user) + }) + + users.PATCH("/:id", func(c *gin.Context) { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"}) + return + } + + var req struct { + Name string `json:"name"` + Email string `json:"email"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + user, err := api.UpdateUser(id, req.Name, req.Email) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, user) + }) + + users.DELETE("/:id", func(c *gin.Context) { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"}) + return + } + + if err := api.DeleteUser(id); err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusNoContent, nil) + }) + } +} +``` + +## Error Handling + +Implement consistent error handling across your API: + +```go +package api + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +type ErrorResponse struct { + Error string `json:"error"` + Code int `json:"code"` + Detail string `json:"detail,omitempty"` +} + +func HandleError(c *gin.Context, statusCode int, message, detail string) { + c.JSON(statusCode, ErrorResponse{ + Error: message, + Code: statusCode, + Detail: detail, + }) +} + +// Usage in handlers +if !found { + HandleError(c, http.StatusNotFound, "Resource not found", "The requested user does not exist") + return +} + +if err := validate(data); err != nil { + HandleError(c, http.StatusBadRequest, "Validation error", err.Error()) + return +} +``` + +## API Middleware + +Add middleware for common concerns like logging and authentication: + +```go +// Logging middleware +func LoggingMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + startTime := time.Now() + + c.Next() + + latency := time.Since(startTime) + log.Printf("[%s] %s %s (%d) - %v", + time.Now().Format(time.RFC3339), + c.Request.Method, + c.Request.URL.Path, + c.Writer.Status(), + latency, + ) + } +} + +// Use in router +router.Use(LoggingMiddleware()) +``` + +## Input Validation + +Always validate input from clients: + +```go +type CreateUserRequest struct { + Name string `json:"name" binding:"required,min=2,max=100"` + Email string `json:"email" binding:"required,email"` + Phone string `json:"phone" binding:"omitempty,max=20"` +} + +func (api *UserAPI) HandleCreateUser(c *gin.Context) { + var req CreateUserRequest + + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + user, err := api.CreateUser(req.Name, req.Email) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusCreated, user) +} +``` + +## Response Formatting + +Use consistent response formats: + +```go +// Successful response +type SuccessResponse struct { + Data interface{} `json:"data"` + Status string `json:"status"` +} + +// List response with pagination +type ListResponse struct { + Data interface{} `json:"data"` + Page int `json:"page"` + PageSize int `json:"pageSize"` + Total int `json:"total"` + HasMore bool `json:"hasMore"` +} +``` + +## Best Practices + + + + - GET for retrieving data + - POST for creating resources + - PATCH for partial updates + - DELETE for removing resources + + + - 200 OK for successful GET + - 201 Created for POST + - 204 No Content for DELETE + - 400 Bad Request for invalid input + - 404 Not Found for missing resources + - 500 Internal Server Error + + + - Validate all input + - Implement rate limiting + - Use HTTPS in production + - Implement authentication + - Sanitize responses + + + - Use pagination for large datasets + - Cache responses appropriately + - Optimize database queries + - Use compression for responses + - Monitor API metrics + + + +## Additional Resources + +- [Gin Framework Documentation](https://github.com/gin-gonic/gin) +- [REST API Best Practices](https://restfulapi.net/) +- [JSON API Specification](https://jsonapi.org/) +- [HTTP Status Codes Reference](https://httpwg.org/specs/rfc9110.html)