Core Concepts
This guide covers the fundamental concepts and patterns used in the goserve framework.
Table of Contents
- Controllers
- Services
- Models
- DTOs (Data Transfer Objects)
- Middleware
- JWT Authentication
- Role-Based Authorization
- Error Handling
- Validation
- Database Connections
- Caching
Controllers
Controllers are responsible for handling HTTP requests and responses. They define API endpoints and delegate business logic to services.
Controller Interface
type Controller interface {
MountRoutes(router *gin.Engine)
}Basic Controller
type controller struct {
network.Controller
common.ContextPayload
service Service
}
func NewController(
authProvider network.AuthenticationProvider,
authorizeProvider network.AuthorizationProvider,
service Service,
) network.Controller {
return &controller{
Controller: network.NewController("/api", authProvider, authorizeProvider),
service: service,
}
}
func (c *controller) MountRoutes(group *gin.RouterGroup) {
group.POST("/", c.createHandler)
group.GET("/id/:id", c.getHandler)
group.PUT("/id/:id", c.updateHandler)
group.DELETE("/id/:id", c.deleteHandler)
}Controller Handler Pattern
func (c *controller) createHandler(ctx *gin.Context) {
// Parse and validate request body
body, err := network.ReqBody[dto.CreateRequest](ctx)
if err != nil {
network.SendBadRequestError(ctx, err.Error(), err)
return
}
// Call service method
result, err := c.service.Create(body)
if err != nil {
network.SendMixedError(ctx, err)
return
}
// Send success response
network.SendSuccessDataResponse(ctx, "success", result)
}Key Responsibilities
- Route Definition - Define HTTP endpoints within feature groups
- Request Parsing - Extract and validate request data using DTOs
- Service Orchestration - Call appropriate service methods
- Response Formatting - Format responses with consistent structure
- Error Handling - Use structured error responses
Services
Services contain business logic and coordinate between controllers and repositories.
Service Pattern
type Service interface {
Create(dto *dto.CreateRequest) (*model.Entity, error)
FindByID(id uuid.UUID) (*model.Entity, error)
Update(dto *dto.UpdateRequest) (*model.Entity, error)
Delete(id uuid.UUID) error
}
type service struct {
db *pgxpool.Pool
cache redis.Cache[dto.EntityCache]
}
func NewService(db *pgxpool.Pool, store redis.Store) Service {
return &service{
db: db,
cache: redis.NewCache[dto.EntityCache](store),
}
}
func (s *service) FindByID(id uuid.UUID) (*model.Entity, error) {
// Try cache first
cached, err := s.cache.Get(id.String())
if err == nil {
return cached, nil
}
// Query database
var entity model.Entity
query := `SELECT * FROM entities WHERE id = $1`
err = s.db.QueryRow(context.Background(), query, id).Scan(&entity)
if err != nil {
return nil, err
}
// Update cache
s.cache.Set(id.String(), &entity, time.Hour)
return &entity, nil
}Service Responsibilities
- Business Logic: Implement domain rules and validation
- Data Access: Coordinate database operations
- Caching: Manage cache invalidation and updates
- Transactions: Handle database transactions
- External Services: Integrate with third-party APIs
- Event Publishing: Send events for system integration
Models
Models represent your database schema and internal domain entities:
Database Models
type Blog struct {
ID uuid.UUID // id - Primary key
Title string // title - Blog title
Description string // description - Blog description
Text *string // text - Published content (nullable)
DraftText string // draft_text - Draft content
Tags []string // tags - JSON array of tags
AuthorID uuid.UUID // author_id - Foreign key to users
Slug string // slug - URL-friendly identifier
Status bool // status - Active/inactive
Published bool // published - Publication status
PublishedAt *time.Time // published_at - Publication timestamp
CreatedAt time.Time // created_at - Creation timestamp
UpdatedAt time.Time // updated_at - Last update timestamp
}
type User struct {
ID uuid.UUID // id - Primary key
Email string // email - Unique email address
Password string // password - Hashed password
Name string // name - Display name
Role string // role - User role (admin, author, editor)
Verified bool // verified - Email verification status
CreatedAt time.Time // created_at - Creation timestamp
UpdatedAt time.Time // updated_at - Last update timestamp
}Model Usage in Services
func (s *service) createBlog(dto *dto.BlogCreate, authorID uuid.UUID) (*model.Blog, error) {
now := time.Now()
blog := &model.Blog{
ID: uuid.New(),
Title: dto.Title,
Description: dto.Description,
DraftText: dto.DraftText,
Tags: dto.Tags,
AuthorID: authorID,
Slug: dto.Slug,
Status: true,
Published: false,
CreatedAt: now,
UpdatedAt: now,
}
// Insert into database
err := s.db.QueryRow(context.Background(), `
INSERT INTO blogs (id, title, description, draft_text, tags, author_id, slug, status, published, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
RETURNING id
`, blog.ID, blog.Title, blog.Description, blog.DraftText, blog.Tags,
blog.AuthorID, blog.Slug, blog.Status, blog.Published, blog.CreatedAt, blog.UpdatedAt).Scan(&blog.ID)
return blog, err
}Model Relationships
// Models with relationships
type BlogWithAuthor struct {
Blog *model.Blog `json:"blog"`
Author *model.User `json:"author"`
}
// Service method with joins
func (s *service) getBlogWithAuthor(id uuid.UUID) (*BlogWithAuthor, error) {
var result BlogWithAuthor
result.Blog = &model.Blog{}
result.Author = &model.User{}
query := `
SELECT
b.id, b.title, b.description, b.text, b.tags, b.slug,
b.published, b.published_at, b.created_at, b.updated_at,
u.id, u.name, u.email
FROM blogs b
JOIN users u ON b.author_id = u.id
WHERE b.id = $1 AND b.status = true
`
err := s.db.QueryRow(context.Background(), query, id).Scan(
&result.Blog.ID, &result.Blog.Title, &result.Blog.Description,
&result.Blog.Text, &result.Blog.Tags, &result.Blog.Slug,
&result.Blog.Published, &result.Blog.PublishedAt,
&result.Blog.CreatedAt, &result.Blog.UpdatedAt,
&result.Author.ID, &result.Author.Name, &result.Author.Email,
)
return &result, err
}Repositories
Repositories abstract data access and provide a clean interface for database operations.
Repository Pattern
goserve services directly use database connections rather than separate repositories, promoting simpler architecture:
type service struct {
db *pgxpool.Pool
}
func (s *service) findEntityByID(ctx context.Context, id uuid.UUID) (*model.Entity, error) {
var entity model.Entity
query := `
SELECT id, name, description, created_at, updated_at
FROM entities
WHERE id = $1 AND deleted_at IS NULL
`
err := s.db.QueryRow(ctx, query, id).Scan(
&entity.ID, &entity.Name, &entity.Description,
&entity.CreatedAt, &entity.UpdatedAt,
)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, network.NewNotFoundError("Entity not found", err)
}
return nil, network.NewInternalServerError("Database error", err)
}
return &entity, nil
}
func (s *service) createEntity(ctx context.Context, entity *model.Entity) (*model.Entity, error) {
query := `
INSERT INTO entities (name, description, created_at, updated_at)
VALUES ($1, $2, $3, $3)
RETURNING id
`
now := time.Now()
err := s.db.QueryRow(ctx, query,
entity.Name, entity.Description, now,
).Scan(&entity.ID)
if err != nil {
return nil, network.NewInternalServerError("Failed to create entity", err)
}
entity.CreatedAt = now
entity.UpdatedAt = now
return entity, nil
}Benefits
- Direct Data Access: Services handle data operations directly
- Transaction Control: Services manage transaction boundaries
- Error Handling: Consistent error types and handling
- Performance: Optimized queries with proper indexing
- Type Safety: Strong typing with UUID and proper Go types
DTOs (Data Transfer Objects)
DTOs define the structure of data transferred between layers.
Request DTOs
goserve uses specific naming conventions for DTOs:
// Create operations
type BlogCreate struct {
Title string `json:"title" validate:"required,min=3,max=500"`
Description string `json:"description" validate:"required,min=3,max=2000"`
DraftText string `json:"draftText" validate:"required,max=50000"`
Slug string `json:"slug" validate:"required,min=3,max=200"`
ImgURL string `json:"imgUrl" validate:"required,uri,max=200"`
Tags []string `json:"tags" validate:"required,min=1,dive,uppercase"`
}
// Update operations
type BlogUpdate struct {
Title *string `json:"title,omitempty" validate:"omitempty,min=3,max=500"`
Description *string `json:"description,omitempty" validate:"omitempty,min=3,max=2000"`
DraftText *string `json:"draftText,omitempty" validate:"omitempty,max=50000"`
Slug *string `json:"slug,omitempty" validate:"omitempty,min=3,max=200"`
Tags []string `json:"tags,omitempty" validate:"omitempty,min=1,dive,uppercase"`
}
// Query parameters
type BlogQuery struct {
Page int `form:"page" validate:"omitempty,min=1"`
Limit int `form:"limit" validate:"omitempty,min=1,max=100"`
Status string `form:"status" validate:"omitempty,oneof=draft published"`
Author string `form:"author" validate:"omitempty,uuid"`
Tag string `form:"tag" validate:"omitempty,max=50"`
}Response DTOs
// Public API responses (hide sensitive data)
type BlogPublic struct {
ID uuid.UUID `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Slug string `json:"slug"`
ImgURL string `json:"imgUrl"`
Tags []string `json:"tags"`
Author *UserInfo `json:"author"`
PublishedAt time.Time `json:"publishedAt"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
// Private API responses (include all data)
type BlogPrivate struct {
ID uuid.UUID `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Text *string `json:"text"`
DraftText string `json:"draftText"`
Slug string `json:"slug"`
ImgURL string `json:"imgUrl"`
Tags []string `json:"tags"`
AuthorID uuid.UUID `json:"authorId"`
Status bool `json:"status"`
Published bool `json:"published"`
PublishedAt *time.Time `json:"publishedAt"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
// Paginated responses
type PaginatedBlogs struct {
Data []BlogPublic `json:"data"`
Pagination Pagination `json:"pagination"`
}
type Pagination struct {
Page int `json:"page"`
Limit int `json:"limit"`
Total int `json:"total"`
TotalPages int `json:"totalPages"`
}Benefits
- Type safety
- Clear API contracts
- Validation rules
- Versioning support
Middleware
Middleware provides cross-cutting concerns like authentication, logging, and error handling.
JWT Authentication Middleware
goserve implements RSA-based JWT authentication:
type authenticationProvider struct {
common.ContextPayload
authService auth.Service
userService user.Service
}
func (m *authenticationProvider) Middleware() gin.HandlerFunc {
return func(ctx *gin.Context) {
// Extract token from Authorization header
token := utils.ExtractBearerToken(ctx.GetHeader("Authorization"))
if token == "" {
network.SendUnauthorizedError(ctx, "Missing authorization token", nil)
return
}
// Verify JWT token with RSA public key
claims, err := m.authService.VerifyToken(token)
if err != nil {
network.SendUnauthorizedError(ctx, err.Error(), err)
return
}
// Load user from database
user, err := m.userService.FetchUserById(claims.Subject)
if err != nil {
network.SendUnauthorizedError(ctx, "User not found", err)
return
}
// Set user in context
m.SetUser(ctx, user)
ctx.Next()
}
}Role-Based Authorization Middleware
type authorizationProvider struct {
common.ContextPayload
}
func (m *authorizationProvider) RequireRole(requiredRoles ...string) gin.HandlerFunc {
return func(ctx *gin.Context) {
user := m.GetUser(ctx)
if user == nil {
network.SendUnauthorizedError(ctx, "User not authenticated", nil)
return
}
// Check if user has required role
hasRole := false
for _, requiredRole := range requiredRoles {
if user.Role == requiredRole {
hasRole = true
break
}
}
if !hasRole {
network.SendForbiddenError(ctx, "Insufficient permissions", nil)
return
}
ctx.Next()
}
}API Key Authentication (for external services)
func (m *authenticationProvider) APIKeyMiddleware() gin.HandlerFunc {
return func(ctx *gin.Context) {
apiKey := ctx.GetHeader("X-API-Key")
if apiKey == "" {
network.SendUnauthorizedError(ctx, "Missing API key", nil)
return
}
// Validate API key (could call auth service)
if !m.isValidAPIKey(apiKey) {
network.SendUnauthorizedError(ctx, "Invalid API key", nil)
return
}
ctx.Next()
}
}Using Middleware in Controllers
func NewController(
authProvider network.AuthenticationProvider,
authorizeProvider network.AuthorizationProvider,
service Service,
) network.Controller {
return &controller{
Controller: network.NewController("/api/blog", authProvider, authorizeProvider),
service: service,
}
}
func (c *controller) MountRoutes(group *gin.RouterGroup) {
// Public routes (no auth required)
group.GET("/public", c.getPublicBlogs)
// Protected routes (auth required)
protected := group.Group("/")
protected.Use(c.AuthProvider.Middleware())
{
protected.POST("/", c.createBlog) // Auth required
// Role-based routes
author := protected.Group("/author")
author.Use(c.AuthorizeProvider.RequireRole("author", "admin"))
{
author.POST("/", c.createBlog)
author.PUT("/id/:id", c.updateBlog)
}
editor := protected.Group("/editor")
editor.Use(c.AuthorizeProvider.RequireRole("editor", "admin"))
{
editor.PUT("/id/:id/publish", c.publishBlog)
}
}
}Built-in Middleware Features
- JWT Authentication: RSA-signed tokens with user context
- Role Authorization: Hierarchical permission system
- API Key Support: Service-to-service authentication
- CORS: Configurable cross-origin settings
- Logging: Structured request/response logging
- Rate Limiting: Configurable request throttling
Dependency Injection
Dependency injection promotes loose coupling and testability.
Constructor Injection
func NewController(
authProvider network.AuthenticationProvider,
authorizeProvider network.AuthorizationProvider,
service Service,
) network.Controller {
return &controller{
Controller: network.NewController("/api", authProvider, authorizeProvider),
service: service,
}
}Benefits
- Easy testing with mocks
- Loose coupling
- Clear dependencies
- Flexible configuration
Error Handling
Consistent error handling across the application.
Error Types
goserve provides structured error types for consistent API responses:
// Client Errors (400-499)
network.SendBadRequestError(ctx, "Invalid input data", validationErr)
network.SendUnauthorizedError(ctx, "Authentication required", nil)
network.SendForbiddenError(ctx, "Insufficient permissions", nil)
network.SendNotFoundError(ctx, "Resource not found", nil)
network.SendConflictError(ctx, "Resource already exists", nil)
// Server Errors (500-599)
network.SendInternalServerError(ctx, "Database connection failed", dbErr)
// Mixed Error (auto-detects error type)
network.SendMixedError(ctx, serviceErr)Service Layer Error Creation
func (s *service) CreateBlog(dto *dto.BlogCreate) (*model.Blog, error) {
// Validate slug uniqueness
existing, err := s.getBlogBySlug(dto.Slug)
if err == nil && existing != nil {
return nil, network.NewBadRequestError("Blog with this slug already exists", nil)
}
// Check authorization
user := s.getCurrentUser()
if user.Role != "author" && user.Role != "admin" {
return nil, network.NewForbiddenError("Only authors can create blogs", nil)
}
// Database operation
blog, err := s.createBlogInDB(dto, user.ID)
if err != nil {
return nil, network.NewInternalServerError("Failed to create blog", err)
}
return blog, nil
}Error Response Format
All errors follow a consistent JSON structure:
// Client Error Response
{
"success": false,
"error": "Bad Request",
"message": "Invalid input data",
"details": {
"field": "email",
"reason": "must be a valid email address"
},
"timestamp": "2024-01-23T10:30:00Z"
}
// Server Error Response (production)
{
"success": false,
"error": "Internal Server Error",
"message": "An unexpected error occurred",
"timestamp": "2024-01-23T10:30:00Z"
}Validation
Request validation using the validator library.
Validation Tags
type CreateUserRequest struct {
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required,min=8"`
Age int `json:"age" validate:"required,min=18,max=120"`
}Validation Middleware
func ValidateRequest(dto interface{}) gin.HandlerFunc {
return func(c *gin.Context) {
if err := c.ShouldBindJSON(dto); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
c.Abort()
return
}
c.Set("dto", dto)
c.Next()
}
}Database Connections
PostgreSQL with Connection Pooling
goserve uses pgx for PostgreSQL with advanced connection pooling:
import "github.com/afteracademy/goserve/postgres"
dbConfig := postgres.DbConfig{
User: env.DBUser,
Pwd: env.DBUserPwd,
Host: env.DBHost,
Port: env.DBPort,
Name: env.DBName,
MinPoolSize: env.DBMinPoolSize,
MaxPoolSize: env.DBMaxPoolSize,
Timeout: time.Duration(env.DBQueryTimeout) * time.Second,
}
db := postgres.NewDatabase(context.Background(), dbConfig)
db.Connect()
defer db.Close()Optimized Query Patterns
// Single row queries
var blog model.Blog
err := db.QueryRow(ctx, `
SELECT id, title, description, author_id, created_at
FROM blogs WHERE id = $1
`, id).Scan(&blog.ID, &blog.Title, &blog.Description, &blog.AuthorID, &blog.CreatedAt)
// Multiple rows with iteration
rows, err := db.Query(ctx, `
SELECT id, title, slug FROM blogs
WHERE published = true ORDER BY created_at DESC
LIMIT $1 OFFSET $2
`, limit, offset)
defer rows.Close()
for rows.Next() {
var blog dto.BlogSummary
rows.Scan(&blog.ID, &blog.Title, &blog.Slug)
blogs = append(blogs, blog)
}
// Insert with RETURNING
err := db.QueryRow(ctx, `
INSERT INTO blogs (title, description, author_id, created_at)
VALUES ($1, $2, $3, $4)
RETURNING id, created_at
`, title, description, authorID, time.Now()).Scan(&blog.ID, &blog.CreatedAt)
// Transactions
tx, err := db.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
// Multiple operations in transaction
_, err = tx.Exec(ctx, "UPDATE blogs SET status = $1 WHERE id = $2", status, id)
if err != nil {
return err
}
_, err = tx.Exec(ctx, "INSERT INTO audit_log (action, blog_id) VALUES ($1, $2)", "publish", id)
if err != nil {
return err
}
return tx.Commit(ctx)Caching
Redis caching for improved performance.
Redis Caching with Generic Types
goserve provides type-safe Redis caching:
import "github.com/afteracademy/goserve/redis"
// Initialize typed cache
type BlogCache struct {
ID uuid.UUID `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Slug string `json:"slug"`
AuthorName string `json:"authorName"`
Tags []string `json:"tags"`
PublishedAt time.Time `json:"publishedAt"`
}
type service struct {
db *pgxpool.Pool
cache redis.Cache[BlogCache]
}
func NewService(db *pgxpool.Pool, store redis.Store) Service {
return &service{
db: db,
cache: redis.NewCache[BlogCache](store),
}
}Cache-Aside Pattern Implementation
func (s *service) GetBlogByID(id uuid.UUID) (*dto.BlogPublic, error) {
// Try cache first
cached, err := s.cache.Get(id.String())
if err == nil && cached != nil {
// Cache hit - convert to DTO
return s.convertCacheToDTO(cached), nil
}
// Cache miss - query database
blog, err := s.getBlogFromDB(id)
if err != nil {
return nil, err
}
// Update cache
cacheData := s.convertBlogToCache(blog)
s.cache.Set(id.String(), cacheData, time.Hour)
return blog, nil
}
func (s *service) UpdateBlog(id uuid.UUID, dto *dto.BlogUpdate) (*dto.BlogPublic, error) {
// Update database
blog, err := s.updateBlogInDB(id, dto)
if err != nil {
return nil, err
}
// Invalidate cache
s.cache.Delete(id.String())
// Update cache with new data
cacheData := s.convertBlogToCache(blog)
s.cache.Set(id.String(), cacheData, time.Hour)
return blog, nil
}Cache Key Patterns
// Structured cache keys
const (
BlogCachePrefix = "blog:"
UserCachePrefix = "user:"
BlogListPrefix = "blog:list:"
)
// Cache key generation
blogKey := fmt.Sprintf("%s%s", BlogCachePrefix, blogID)
userKey := fmt.Sprintf("%s%s", UserCachePrefix, userID)
blogListKey := fmt.Sprintf("%sauthor:%s:page:%d", BlogListPrefix, authorID, page)
// Bulk operations
func (s *service) invalidateUserBlogs(userID uuid.UUID) error {
// Find all blog IDs for user
blogIDs, err := s.getUserBlogIDs(userID)
if err != nil {
return err
}
// Delete cache entries
keys := make([]string, len(blogIDs))
for i, id := range blogIDs {
keys[i] = fmt.Sprintf("%s%s", BlogCachePrefix, id)
}
return s.cache.DeleteMultiple(keys...)
}Cache Configuration
// Environment-based cache TTL
blogCacheTTL := time.Duration(env.BlogCacheTTLMinutes) * time.Minute
userCacheTTL := time.Duration(env.UserCacheTTLMinutes) * time.Minute
// Cache with custom TTL
s.cache.Set(blogKey, blogData, blogCacheTTL)Cache Patterns Supported
- Cache-Aside: Load from cache, fallback to DB
- Write-Through: Update DB and cache simultaneously
- Write-Behind: Update DB, invalidate cache (simpler)
- Time-Based Expiration: Automatic cache invalidation
- Manual Invalidation: Explicit cache clearing on updates
Best Practices
Architecture Principles
- Feature-Based Organization - Group related functionality by business features, not technical layers
- Controller-Service-Model-DTO Pattern - Maintain clear separation of concerns across layers
- Dependency Injection - Use module system for clean component wiring and testability
- Interface-Based Design - Define service interfaces for easy mocking and testing
Controller Best Practices
- Keep Controllers Thin - Controllers should only handle HTTP concerns (parsing, validation, response formatting)
- Use DTOs for Requests/Responses - Never expose internal models directly to clients
- Consistent Error Handling - Use goserve's structured error responses
- Route Organization - Group routes by authentication and authorization requirements
Service Best Practices
- Business Logic Only - Services should contain domain logic, not infrastructure concerns
- Transaction Management - Handle database transactions at the service level
- Cache Integration - Implement cache-aside pattern for performance
- Error Propagation - Use appropriate error types for different failure scenarios
Database Best Practices
- Connection Pooling - Always use connection pools for database access
- Prepared Statements - Use parameterized queries to prevent SQL injection
- Indexing Strategy - Design indexes based on query patterns
- Transaction Boundaries - Keep transactions short and focused
Security Best Practices
- JWT with RSA - Use RSA key pairs for JWT signing (not symmetric keys)
- Password Hashing - Always hash passwords with strong algorithms (bcrypt)
- Input Validation - Validate all inputs at multiple layers
- API Key Management - Use API keys for service-to-service communication
Caching Best Practices
- Cache-Aside Pattern - Load from cache, fallback to database
- Appropriate TTL - Set cache expiration based on data volatility
- Cache Invalidation - Invalidate cache on data updates
- Cache Key Strategy - Use structured, predictable cache keys
Testing Best Practices
- Unit Tests - Test services and utilities with mocks
- Integration Tests - Test complete request/response cycles
- Test Data Management - Use fixtures and cleanup for reliable tests
- Coverage Goals - Aim for high coverage on business logic
Performance Best Practices
- Database Optimization - Use proper indexing and query optimization
- Caching Strategy - Cache frequently accessed data
- Connection Pooling - Configure appropriate pool sizes
- Async Processing - Use background jobs for non-critical operations
Next Steps
- See Framework Architecture for overall structure
- Check Configuration for setup details
- Review the PostgreSQL Example for complete examples
