example

package
v1.0.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 20, 2026 License: MIT Imports: 11 Imported by: 0

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

Constants

This section is empty.

Variables

View Source
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

type CreateParams struct {
	Email string
	Name  string
	Role  UserRole
}

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 NewHandler

func NewHandler(service UserService) *Handler

NewHandler creates a new Handler.

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

type ListFilter struct {
	Role   *UserRole
	Active *bool
}

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

func (r *Repository) FindByEmail(ctx context.Context, email string) (*User, error)

FindByEmail looks up a user by email address.

func (*Repository) FindByID

func (r *Repository) FindByID(ctx context.Context, id uint) (*User, error)

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.

func (*Repository) Update

func (r *Repository) Update(ctx context.Context, user *User) error

Update saves changes to an existing user.

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

func (s *Service) Create(ctx context.Context, params CreateParams) (*User, error)

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) Delete

func (s *Service) Delete(ctx context.Context, id uint) error

Delete removes a user by ID.

func (*Service) GetByID

func (s *Service) GetByID(ctx context.Context, id uint) (*User, error)

GetByID returns a user by primary key.

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.

func (*Service) Update

func (s *Service) Update(ctx context.Context, id uint, params UpdateParams, callerID uint) (*User, error)

Update modifies user fields.

Business rules:

  • A regular user cannot change their own role.
  • A user cannot deactivate themselves (callerID == id && Active = false).

type UpdateParams

type UpdateParams struct {
	Name   *string
	Role   *UserRole
	Active *bool
}

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.

const (
	RoleUser  UserRole = "user"
	RoleAdmin UserRole = "admin"
)

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL