Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/egeuysall/ryva-archive/llms.txt

Use this file to discover all available pages before exploring further.

Overview

Every backend module in Ryva follows a consistent Handler → Service → Repository architecture. This pattern provides clear separation of concerns and makes the codebase predictable and maintainable.

Three-Layer Architecture

1

Handler Layer

Handles HTTP requests and responses
  • Extracts data from requests
  • Validates input format
  • Calls service layer
  • Formats responses
2

Service Layer

Contains business logic
  • Validates business rules
  • Orchestrates multiple operations
  • Handles authorization
  • Manages transactions
3

Repository Layer

Manages data access
  • Executes database queries
  • Converts between DB and domain models
  • Handles database errors

Module File Structure

Each module typically contains these files:
modules/auth/
├── handler.go          # HTTP request handlers
├── service.go          # Business logic
├── repository.go       # Database operations
├── models.go          # Domain models and converters
├── dto.go             # Request/response DTOs
├── *_test.go          # Unit tests
└── *_integration_test.go  # Integration tests

Layer Responsibilities

Handler Layer

Purpose: Handle HTTP protocol details without business logic
Handlers are responsible for:
  • Decoding request bodies
  • Extracting path parameters and query strings
  • Authentication context extraction
  • Logging HTTP-specific details
  • Error response formatting
Example Handler (internal/modules/auth/handler.go):
type Handler struct {
    service *Service
}

func NewHandler(service *Service) *Handler {
    return &Handler{service: service}
}

// GetMe handles GET /api/v1/auth/me
func (h *Handler) GetMe(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()

    // Extract user ID from auth middleware context
    userID, err := appcontext.GetUserID(ctx)
    if err != nil {
        httputil.RespondWithError(w, r, err)
        return
    }

    // Call service layer
    user, err := h.service.GetMeWithOrganizations(ctx, userID)
    if err != nil {
        logError(ctx, "Failed to get user", err, map[string]any{
            "user_id": userID.String(),
        })
        httputil.RespondWithError(w, r, err)
        return
    }

    // Convert to response DTO
    resp := toGetMeResponse(user)
    httputil.OK(w, r, resp)
}
  • Keep handlers thin - Only HTTP concerns, no business logic
  • Use helper functions - Extract repetitive code (logging, error handling)
  • Consistent error handling - Use httputil.RespondWithError() for all errors
  • Log appropriately - Log requests with context, errors with severity
  • Use DTOs - Don’t expose internal models directly in responses

Service Layer

Purpose: Implement business rules and orchestrate operations
Services are responsible for:
  • Business rule validation
  • Authorization checks
  • Cross-domain coordination
  • Transaction management
  • Calling external services
Example Service (internal/modules/auth/service.go):
type Service struct {
    repo RepositoryInterface
}

func NewService(repo RepositoryInterface) *Service {
    return &Service{repo: repo}
}

// UpdateProfile updates a user's profile information
func (s *Service) UpdateProfile(
    ctx context.Context,
    userID uuid.UUID,
    req UpdateProfileRequest,
) (*User, error) {
    // Business validation
    if req.FullName != nil {
        trimmed := strings.TrimSpace(*req.FullName)
        if trimmed == "" {
            return nil, apperrors.InvalidInput("full name cannot be empty")
        }
        if len(trimmed) > 255 {
            return nil, apperrors.InvalidInput("full name cannot exceed 255 characters")
        }
        req.FullName = &trimmed
    }

    if req.AvatarURL != nil {
        trimmed := strings.TrimSpace(*req.AvatarURL)
        if trimmed != "" {
            if !strings.HasPrefix(trimmed, "http://") && 
               !strings.HasPrefix(trimmed, "https://") {
                return nil, apperrors.InvalidInput("avatar URL must be a valid HTTP(S) URL")
            }
        }
    }

    // Business rule: at least one field required
    if req.FullName == nil && req.AvatarURL == nil {
        return nil, apperrors.InvalidInput("at least one field must be provided")
    }

    // Call repository
    user, err := s.repo.UpdateUserProfile(ctx, userID, req.FullName, req.AvatarURL)
    if err != nil {
        return nil, err
    }

    return user, nil
}
  • Validate business rules - Not just input format, but business constraints
  • Return domain errors - Use apperrors package for standardized errors
  • Keep database-agnostic - Services shouldn’t know about SQL or pgx
  • Use interfaces - Depend on repository interfaces for testability
  • Handle transactions - Coordinate multi-step operations safely

Repository Layer

Purpose: Abstract database access and handle data mapping
Repositories are responsible for:
  • Executing SQLC-generated queries
  • Converting between database and domain models
  • Handling database-specific errors
  • Managing database transactions
Repository Interface (internal/modules/auth/repository.go):
type RepositoryInterface interface {
    GetUserByID(ctx context.Context, userID uuid.UUID) (*User, error)
    GetUserByEmail(ctx context.Context, email string) (*User, error)
    GetUserWithOrganizations(ctx context.Context, userID uuid.UUID) (*UserWithOrganizations, error)
    UpdateUserProfile(ctx context.Context, userID uuid.UUID, fullName, avatarURL *string) (*User, error)
    CompleteOnboarding(ctx context.Context, userID uuid.UUID) (*User, error)
    GetUserPreferences(ctx context.Context, userID uuid.UUID) (map[string]any, error)
    UpdateUserPreferences(ctx context.Context, userID uuid.UUID, preferences map[string]any) (*User, error)
}
Repository Implementation:
type Repository struct {
    pool    *pgxpool.Pool
    queries *auth.Queries  // SQLC-generated
}

func NewRepository(pool *pgxpool.Pool) *Repository {
    return &Repository{
        pool:    pool,
        queries: auth.New(pool),
    }
}

// GetUserByID retrieves a user by ID
func (r *Repository) GetUserByID(ctx context.Context, userID uuid.UUID) (*User, error) {
    // Call SQLC-generated query
    dbUser, err := r.queries.GetUserByID(ctx, UUIDToPgUUID(userID))
    if err != nil {
        if errors.Is(err, pgx.ErrNoRows) {
            return nil, apperrors.NotFound("User not found")
        }
        return nil, apperrors.DatabaseError("failed to get user by ID").WithInternal(err)
    }

    // Convert DB model to domain model
    return FromDB(&dbUser)
}
  • Use SQLC queries - Never write raw SQL strings in Go code
  • Convert errors - Map pgx.ErrNoRows to apperrors.NotFound
  • Type conversion - Handle UUID and nullable types properly
  • Define interfaces - Allow service layer to mock repositories
  • Keep thin - Don’t add business logic here

Model Conversion

Each module defines converters between database and domain models: models.go Pattern:
// Domain model (used by service layer)
type User struct {
    ID                  uuid.UUID
    Email               string
    FullName            *string
    AvatarURL           *string
    OnboardingCompleted bool
    Preferences         map[string]any
    CreatedAt           time.Time
    UpdatedAt           time.Time
}

// FromDB converts database model to domain model
func FromDB(dbUser *auth.User) (*User, error) {
    return &User{
        ID:                  PgUUIDToUUID(dbUser.ID),
        Email:               dbUser.Email,
        FullName:            PgTextToString(dbUser.FullName),
        AvatarURL:           PgTextToString(dbUser.AvatarUrl),
        OnboardingCompleted: dbUser.OnboardingCompleted,
        Preferences:         ParsePreferences(dbUser.Preferences),
        CreatedAt:           dbUser.CreatedAt.Time,
        UpdatedAt:           dbUser.UpdatedAt.Time,
    }, nil
}

// Helper functions for type conversion
func UUIDToPgUUID(id uuid.UUID) pgtype.UUID {
    return pgtype.UUID{Bytes: id, Valid: true}
}

func PgUUIDToUUID(id pgtype.UUID) uuid.UUID {
    return uuid.UUID(id.Bytes)
}

func StringToPgText(s *string) pgtype.Text {
    if s == nil {
        return pgtype.Text{Valid: false}
    }
    return pgtype.Text{String: *s, Valid: true}
}

DTOs (Data Transfer Objects)

Separate types for requests and responses: dto.go Pattern:
// Request DTO
type UpdateProfileRequest struct {
    FullName  *string `json:"full_name,omitempty"`
    AvatarURL *string `json:"avatar_url,omitempty"`
}

// Response DTO
type GetMeResponse struct {
    ID                  string                      `json:"id"`
    Email               string                      `json:"email"`
    FullName            *string                     `json:"full_name"`
    AvatarURL           *string                     `json:"avatar_url"`
    OnboardingCompleted bool                        `json:"onboarding_completed"`
    Preferences         map[string]any              `json:"preferences"`
    Organizations       []OrganizationMembershipDTO `json:"organizations"`
    CreatedAt           string                      `json:"created_at"`
    UpdatedAt           string                      `json:"updated_at"`
}

Creating a New Module

1

Create module directory

mkdir -p internal/modules/mymodule
2

Define repository interface

Create repository.go with data access interface and implementation:
type RepositoryInterface interface {
    // Define your data access methods
}

type Repository struct {
    pool    *pgxpool.Pool
    queries *mymodule.Queries
}
3

Implement service layer

Create service.go with business logic:
type Service struct {
    repo RepositoryInterface
}

func NewService(repo RepositoryInterface) *Service {
    return &Service{repo: repo}
}
4

Add HTTP handlers

Create handler.go with HTTP endpoints:
type Handler struct {
    service *Service
}

func NewHandler(service *Service) *Handler {
    return &Handler{service: service}
}
5

Register routes

Add routes in internal/router/router.go:
r.Route("/mymodule", func(r chi.Router) {
    r.Use(authMiddleware)
    r.Get("/", handlers.MyModule.List)
    r.Post("/", handlers.MyModule.Create)
})

Testing

Each layer should have dedicated tests:
Test HTTP handling with mock services:
func TestHandler_GetMe(t *testing.T) {
    mockService := &MockService{}
    handler := NewHandler(mockService)
    // Test request handling
}
Test business logic with mock repositories:
func TestService_UpdateProfile(t *testing.T) {
    mockRepo := &MockRepository{}
    service := NewService(mockRepo)
    // Test business rules
}
Test database operations (integration tests):
func TestRepository_GetUserByID(t *testing.T) {
    db := setupTestDB(t)
    repo := NewRepository(db)
    // Test actual database queries
}

Next Steps

Backend Structure

Review overall project organization

Database Setup

Learn about SQLC and database queries

Build docs developers (and LLMs) love