Documentation
¶
Overview ¶
Package example demonstrates a complete package structure in Clean Architecture.
This package is the canonical reference for teams using portsmith. Each file owns exactly one layer. Dependencies point inward only:
Handler → ServicePort ← Service → RepositoryPort ← Repository
Files ordered from the core outward:
domain.go — domain types (core, zero dependencies) errors.go — domain errors ports.go — port interfaces (normally generated by portsmith gen) service.go — business logic repository.go — storage adapter handler.go — HTTP adapter dto.go — request/response structs mappers.go — domain ↔ DTO conversion
Index ¶
- Variables
- type CreateParams
- type CreateUserRequest
- type Handler
- type ListFilter
- type ListUsersResponse
- type Repository
- func (r *Repository) Create(ctx context.Context, user *User) error
- func (r *Repository) Delete(ctx context.Context, id uint) error
- func (r *Repository) FindByEmail(ctx context.Context, email string) (*User, error)
- func (r *Repository) FindByID(ctx context.Context, id uint) (*User, error)
- func (r *Repository) List(ctx context.Context, filter ListFilter, page pagination.OffsetPage) ([]*User, int64, error)
- func (r *Repository) Update(ctx context.Context, user *User) error
- type Service
- func (s *Service) Create(ctx context.Context, params CreateParams) (*User, error)
- func (s *Service) Delete(ctx context.Context, id uint) error
- func (s *Service) GetByID(ctx context.Context, id uint) (*User, error)
- func (s *Service) List(ctx context.Context, filter ListFilter, page pagination.OffsetPage) ([]*User, int64, error)
- func (s *Service) Update(ctx context.Context, id uint, params UpdateParams, callerID uint) (*User, error)
- type UpdateParams
- type UpdateUserRequest
- type User
- type UserRepository
- type UserResponse
- type UserRole
- type UserService
Constants ¶
This section is empty.
Variables ¶
var ( // ErrUserNotFound is returned when a user cannot be found by ID or email. ErrUserNotFound = apperrors.NotFound("user not found") // ErrEmailTaken is returned when creating a user with an email address // that is already registered. ErrEmailTaken = apperrors.Conflict("email already taken") // ErrCannotDeactivateSelf is returned when an administrator tries to // deactivate their own account. ErrCannotDeactivateSelf = apperrors.BadRequest("cannot deactivate your own account") // ErrInsufficientPermissions is returned when an operation requires // administrator privileges. ErrInsufficientPermissions = apperrors.Forbidden("insufficient permissions") )
Functions ¶
This section is empty.
Types ¶
type CreateParams ¶
CreateParams carries the inputs needed to create a new user. The handler converts a DTO → CreateParams; the service receives CreateParams.
Why not pass the DTO directly into the service? The service must not know about HTTP-specific concerns (json tags, validator tags). CreateParams is the pure "language" of the business operation.
type CreateUserRequest ¶
type CreateUserRequest struct {
Email string `json:"email" binding:"required,email"`
Name string `json:"name" binding:"required,min=2,max=100"`
Role UserRole `json:"role" binding:"omitempty,oneof=user admin"`
}
CreateUserRequest is the body for POST /users.
type Handler ¶
type Handler struct {
// contains filtered or unexported fields
}
Handler implements the HTTP endpoints for user management.
func (*Handler) Routes ¶
func (h *Handler) Routes(rg *gin.RouterGroup)
Routes registers the handler's endpoints in the provided Gin router group. Called from main.go or during server setup.
Example:
v1 := srv.Router().Group("/api/v1")
userHandler.Routes(v1)
type ListFilter ¶
ListFilter holds optional filtering criteria for list queries.
type ListUsersResponse ¶
type ListUsersResponse struct {
Items []*UserResponse `json:"items"`
Total int64 `json:"total"`
Page int `json:"page"`
Limit int `json:"limit"`
}
ListUsersResponse is the response for GET /users including pagination metadata.
type Repository ¶
type Repository struct {
// contains filtered or unexported fields
}
Repository implements UserRepository on top of GORM.
func NewRepository ¶
func NewRepository(db *gorm.DB) *Repository
NewRepository creates a new Repository.
func (*Repository) Create ¶
func (r *Repository) Create(ctx context.Context, user *User) error
Create persists a new user to the database.
func (*Repository) Delete ¶
func (r *Repository) Delete(ctx context.Context, id uint) error
Delete removes a user by ID (soft delete if the model has a DeletedAt field).
func (*Repository) FindByEmail ¶
FindByEmail looks up a user by email address.
func (*Repository) FindByID ¶
FindByID returns a user by primary key. Returns ErrUserNotFound when the record does not exist — never gorm.ErrRecordNotFound.
func (*Repository) List ¶
func (r *Repository) List(ctx context.Context, filter ListFilter, page pagination.OffsetPage) ([]*User, int64, error)
List returns a page of users with optional filtering.
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service implements the business logic for user management.
func NewService ¶
func NewService(repo UserRepository) *Service
NewService creates a new Service. It accepts an interface — easy to test.
func (*Service) Create ¶
Create creates a new user.
Business rules:
- Email must be unique (verified through the repository).
- If no role is provided, RoleUser is assigned by default.
func (*Service) List ¶
func (s *Service) List(ctx context.Context, filter ListFilter, page pagination.OffsetPage) ([]*User, int64, error)
List returns a filtered, paginated page of users.
type UpdateParams ¶
UpdateParams carries the inputs for a partial update. Pointer fields mean "field was provided" vs "field was omitted" (partial update).
type UpdateUserRequest ¶
type UpdateUserRequest struct {
Name *string `json:"name" binding:"omitempty,min=2,max=100"`
Role *UserRole `json:"role" binding:"omitempty,oneof=user admin"`
Active *bool `json:"active"`
}
UpdateUserRequest is the body for PATCH /users/:id. Pointer fields allow distinguishing "field not sent" from "field sent as empty".
type User ¶
type User struct {
ID uint `gorm:"primaryKey"`
Email string `gorm:"uniqueIndex;not null"`
Name string `gorm:"not null"`
Role UserRole `gorm:"default:'user'"`
Active bool `gorm:"default:true"`
CreatedAt time.Time
UpdatedAt time.Time
}
User is the central domain entity of this package.
Rule: domain.go must not import database/sql, net/http, or gorm. These are plain Go types. They are safe to pass between all layers.
GORM tags (`gorm:"..."`) are a pragmatic compromise that enables AutoMigrate. In a strict Clean Architecture the DB model would live in repository.go as a separate struct. For most projects the tags on the domain type are sufficient; split only when the DB schema diverges meaningfully from the domain.
type UserRepository ¶
type UserRepository interface {
Create(ctx context.Context, user *User) error
FindByID(ctx context.Context, id uint) (*User, error)
FindByEmail(ctx context.Context, email string) (*User, error)
Update(ctx context.Context, user *User) error
Delete(ctx context.Context, id uint) error
List(ctx context.Context, filter ListFilter, page pagination.OffsetPage) ([]*User, int64, error)
}
UserRepository is the storage port used by the service.
Service depends on this interface, not on the concrete *Repository. This allows unit-testing the service without a database — a mock is enough.
Compile-time guard: var _ UserRepository = (*Repository)(nil) If Repository does not satisfy the interface, the error is a compile error, not a runtime panic.
type UserResponse ¶
type UserResponse struct {
ID uint `json:"id"`
Email string `json:"email"`
Name string `json:"name"`
Role UserRole `json:"role"`
Active bool `json:"active"`
}
UserResponse is the user representation returned in API responses. Sensitive fields (passwords, tokens) are intentionally excluded.
type UserRole ¶
type UserRole string
UserRole is a role enum. It is defined in the domain, not in the database.
type UserService ¶
type UserService interface {
Create(ctx context.Context, params CreateParams) (*User, error)
GetByID(ctx context.Context, id uint) (*User, error)
Update(ctx context.Context, id uint, params UpdateParams, callerID uint) (*User, error)
Delete(ctx context.Context, id uint) error
List(ctx context.Context, filter ListFilter, page pagination.OffsetPage) ([]*User, int64, error)
}
UserService is the business-logic port used by the handler.
Handler depends on this interface, not on the concrete *Service. Enables handler unit tests without a service or a database.