Common Patterns
This guide covers common patterns and best practices for building applications with forge.
Model Patterns​
Soft Delete Pattern​
Implement soft deletes by adding a deleted_at field:
func (Post) Fields() []schema.Field {
return []schema.Field{
// ... other fields ...
schema.Time("deleted_at").Null().Build(),
}
}
// Custom QuerySet method
func (qs *PostQuerySet) Active(ctx context.Context) (*PostQuerySet, error) {
return qs.Filter(Post.Fields.DeletedAt.IsNull()), nil
}
Timestamps Pattern​
Add created/updated timestamps to all models:
func (Post) Fields() []schema.Field {
return []schema.Field{
// ... other fields ...
schema.Time("created_at").AutoNowAdd().Build(),
schema.Time("updated_at").AutoNow().Build(),
}
}
UUID Primary Keys​
Use UUIDs instead of auto-incrementing integers:
func (Post) Fields() []schema.Field {
return []schema.Field{
schema.UUID("id").Primary().Build(),
// ... other fields ...
}
}
Query Patterns​
Pagination Pattern​
Implement consistent pagination:
type PaginatedResult[T any] struct {
Items []*T
Total int64
Page int
PageSize int
TotalPages int
}
func Paginate[T any](
qs QuerySet[T],
page, pageSize int,
ctx context.Context,
) (*PaginatedResult[T], error) {
total, err := qs.Count(ctx)
if err != nil {
return nil, err
}
items, err := qs.
Limit(pageSize).
Offset((page - 1) * pageSize).
All(ctx)
if err != nil {
return nil, err
}
totalPages := int(math.Ceil(float64(total) / float64(pageSize)))
return &PaginatedResult[T]{
Items: items,
Total: total,
Page: page,
PageSize: pageSize,
TotalPages: totalPages,
}, nil
}
Search Pattern​
Implement full-text search:
func SearchPosts(query string, ctx context.Context) ([]*Post, error) {
return Post.Objects.
Filter(
Post.Fields.Title.Contains(query).
Or(Post.Fields.Content.Contains(query)),
).
OrderBy("-created_at").
All(ctx)
}
Filter Chain Pattern​
Build dynamic filters:
func BuildPostFilters(filters map[string]interface{}) QueryExpr {
var conditions []QueryExpr
if published, ok := filters["published"].(bool); ok {
conditions = append(conditions, Post.Fields.Published.Equals(published))
}
if authorID, ok := filters["author_id"].(int64); ok {
conditions = append(conditions, Post.Fields.AuthorID.Equals(authorID))
}
if len(conditions) == 0 {
return nil
}
result := conditions[0]
for i := 1; i < len(conditions); i++ {
result = result.And(conditions[i])
}
return result
}
Service Layer Pattern​
Service Interface​
Define service interfaces:
type PostService interface {
CreatePost(ctx context.Context, req *CreatePostRequest) (*Post, error)
UpdatePost(ctx context.Context, id int64, req *UpdatePostRequest) (*Post, error)
DeletePost(ctx context.Context, id int64) error
GetPost(ctx context.Context, id int64) (*Post, error)
ListPosts(ctx context.Context, filters *PostFilters) ([]*Post, error)
}
Service Implementation​
Implement services with business logic:
type postService struct {
db *db.DB
}
func NewPostService(db *db.DB) PostService {
return &postService{db: db}
}
func (s *postService) CreatePost(ctx context.Context, req *CreatePostRequest) (*Post, error) {
// Validate request
if req.Title == "" {
return nil, errors.New("title is required")
}
// Create post
post := &Post{
Title: req.Title,
Content: req.Content,
Published: req.Published,
}
err := Post.Objects.Create(ctx, post)
if err != nil {
return nil, err
}
return post, nil
}
Handler Patterns​
HTTP Handler Pattern​
Create consistent HTTP handlers:
func ListPostsHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// Parse query parameters
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
if page < 1 {
page = 1
}
// Get posts
result, err := Paginate(Post.Objects, page, 20, ctx)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Serialize response
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(result)
}
Error Handling Pattern​
Consistent error responses:
type ErrorResponse struct {
Error string `json:"error"`
Details map[string]string `json:"details,omitempty"`
}
func SendError(w http.ResponseWriter, status int, message string, details ...map[string]string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
resp := ErrorResponse{Error: message}
if len(details) > 0 {
resp.Details = details[0]
}
json.NewEncoder(w).Encode(resp)
}
Validation Patterns​
Model Validation​
Validate in hooks:
func (Post) Hooks() *schema.ModelHooks {
return &schema.ModelHooks{
Clean: func(ctx context.Context, instance interface{}) error {
post := instance.(*Post)
if post.Title == "" {
return errors.New("title is required")
}
if len(post.Title) > 200 {
return errors.New("title must be 200 characters or less")
}
return nil
},
}
}
Request Validation​
Validate API requests:
type CreatePostRequest struct {
Title string `json:"title" validate:"required,max=200"`
Content string `json:"content" validate:"required"`
Published bool `json:"published"`
}
func ValidateCreatePostRequest(req *CreatePostRequest) error {
if req.Title == "" {
return errors.New("title is required")
}
if len(req.Title) > 200 {
return errors.New("title must be 200 characters or less")
}
return nil
}
Transaction Patterns​
Transaction Wrapper​
Wrap operations in transactions:
func CreatePostWithTags(ctx context.Context, post *Post, tagIDs []int64) error {
return db.WithTx(ctx, func(tx *db.Tx) error {
// Create post
if err := Post.Objects.Create(ctx, post); err != nil {
return err
}
// Create tag associations
for _, tagID := range tagIDs {
association := &PostTag{
PostID: post.ID,
TagID: tagID,
}
if err := PostTag.Objects.Create(ctx, association); err != nil {
return err
}
}
return nil
})
}
Caching Patterns​
Query Result Caching​
Cache expensive queries:
func GetPopularPosts(ctx context.Context) ([]*Post, error) {
cacheKey := "popular_posts"
// Try cache
if cached, err := cache.Get(ctx, cacheKey); err == nil {
return cached.([]*Post), nil
}
// Query database
posts, err := Post.Objects.
Filter(Post.Fields.Published.Equals(true)).
OrderBy("-view_count").
Limit(10).
All(ctx)
if err != nil {
return nil, err
}
// Cache result
cache.Set(ctx, cacheKey, posts, 5*time.Minute)
return posts, nil
}
Testing Patterns​
Test Setup Pattern​
Consistent test setup:
func setupTestDB(t *testing.T) *db.DB {
db, err := db.NewDB("postgres://test:test@localhost/testdb?sslmode=disable")
if err != nil {
t.Fatal(err)
}
// Run migrations
if err := migrate.Up(db); err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
migrate.Down(db)
db.Close()
})
return db
}
Test Data Factory​
Create test data:
func CreateTestPost(t *testing.T, db *db.DB) *Post {
post := &Post{
Title: "Test Post",
Content: "Test Content",
Published: true,
}
err := Post.Objects.Create(context.Background(), post)
require.NoError(t, err)
return post
}
Best Practices​
- Use Services for Business Logic - Keep handlers thin
- Validate Early - Validate in multiple layers
- Use Transactions - For multi-step operations
- Cache Expensive Queries - But invalidate properly
- Handle Errors Gracefully - Return user-friendly messages
- Use Type Safety - Prefer type-safe API over dynamic
- Write Tests - Test business logic thoroughly
- Document Patterns - Share patterns with your team
Next Steps​
- Best Practices - More best practices
- Performance Guide - Performance optimization
- Security Guide - Security patterns