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
Handler Layer
Handles HTTP requests and responses
Extracts data from requests
Validates input format
Calls service layer
Formats responses
Service Layer
Contains business logic
Validates business rules
Orchestrates multiple operations
Handles authorization
Manages transactions
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 )
}
Repository Best Practices
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
Create module directory
mkdir -p internal/modules/mymodule
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
}
Implement service layer
Create service.go with business logic: type Service struct {
repo RepositoryInterface
}
func NewService ( repo RepositoryInterface ) * Service {
return & Service { repo : repo }
}
Add HTTP handlers
Create handler.go with HTTP endpoints: type Handler struct {
service * Service
}
func NewHandler ( service * Service ) * Handler {
return & Handler { service : service }
}
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