Project Architecture
Understanding the goserve example API architecture and design patterns.
Overview
The goserve PostgreSQL example demonstrates a complete production-ready REST API built with the goserve framework. It follows a feature-based modular architecture where each API endpoint is organized into self-contained modules with clear separation of concerns, JWT authentication, role-based authorization, and comprehensive testing.
Core Principles
- Feature Independence - Each API feature is isolated in its own directory
- Service Sharing - Common services can be shared across features
- Clean Separation - Controllers, services, models, and DTOs are clearly separated
- Testability - Architecture supports easy unit and integration testing
Directory Structure
goserve-example-api-server-postgres/
├── api/ # API feature modules
│ ├── auth/ # Authentication
│ │ ├── dto/ # Data Transfer Objects
│ │ ├── model/ # Database models
│ │ ├── middleware/ # Auth middleware
│ │ ├── controller.go # HTTP handlers
│ │ ├── service.go # Business logic
│ │ └── mock.go # Test mocks
│ ├── blog/ # Blog operations
│ │ ├── author/ # Author-specific endpoints
│ │ ├── editor/ # Editor-specific endpoints
│ │ ├── dto/
│ │ ├── model/
│ │ ├── controller.go
│ │ └── service.go
│ ├── blogs/ # Public blog listing
│ ├── contact/ # Contact form
│ └── user/ # User management
├── cmd/ # Application entry point
│ └── main.go # Main function
├── common/ # Shared utilities
│ └── payload.go # Request context helpers
├── config/ # Configuration
│ └── env.go # Environment variables
├── startup/ # Server initialization
│ ├── server.go # Server setup
│ ├── module.go # Dependency injection
│ └── testserver.go # Test server
├── tests/ # Integration tests
├── utils/ # Utility functions
├── .tools/ # Code generation tools
│ ├── apigen.go # API generator
│ ├── rsa/ # RSA key generator
│ └── copy/ # Env file copier
├── keys/ # RSA keys for JWT
├── .extra/ # Database scripts, docs
├── docker-compose.yml # Docker configuration
└── Dockerfile # Container imageApplication Flow
Startup Sequence
main.go (cmd/main.go)
↓
startup.Server() - Initialize HTTP server
↓
create() - Component initialization
├── Load Environment Variables (config.Env)
│ ├── Database credentials
│ ├── JWT RSA keys
│ ├── Redis configuration
│ └── Server settings
├── Connect PostgreSQL (postgres.Database)
│ ├── Create connection pool
│ ├── Configure timeouts
│ └── Health checks
├── Connect Redis (redis.Store)
│ ├── Initialize client
│ ├── Configure pooling
│ └── Test connection
├── Create Module (startup.Module)
│ ├── Wire Dependencies
│ ├── Initialize Services
│ ├── Create Controllers
│ └── Setup Middleware
↓
router.Start() - Start Gin HTTP server
├── Global middleware (CORS, logging, error handling)
├── Route mounting (/auth, /user, /blog, etc.)
└── Server listening on configured portComplete Request Flow
HTTP Request (e.g., POST /blog/author)
↓
Root Middleware (Global - applied to all routes)
├── Error Recovery - Catch panics and return 500
├── API Key Validation - For external service calls
├── CORS Headers - Cross-origin resource sharing
├── Request Logging - Structured logging
└── Not Found Handler - 404 for undefined routes
↓
Router (Gin Engine)
↓
Feature Route Group (/blog)
↓
Authentication Middleware (JWT - if route requires auth)
├── Extract Bearer Token - From Authorization header
├── Verify RSA Signature - Using public key
├── Validate Claims - Check expiry, issuer, etc.
├── Load User from Database - Fetch user details
└── Set User Context - Store user in request context
↓
Authorization Middleware (Roles - if route requires specific roles)
├── Get User from Context - Retrieve authenticated user
├── Check Required Roles - Compare with route requirements
├── Validate Permissions - Role-based access control
└── Allow/Deny Access - Proceed or return 403
↓
Controller Handler (blog.controller.createHandler)
├── Parse Request Body - JSON to DTO with validation
├── Extract Path Parameters - URL parameters (e.g., :id)
├── Extract Query Parameters - Query string parameters
├── Validate Input - Struct field validation
└── Call Service Method - Delegate to business logic
↓
Service Layer (Business Logic)
├── Input Validation - Business rule validation
├── Database Operations - CRUD operations with transactions
├── Cache Operations - Redis cache get/set/invalidate
├── External Service Calls - API calls to other services
└── Event Publishing - Send events for system integration
↓
Database/External Services
↓
Response Formation
├── Success Response (200-299)
│ ├── network.SendSuccessDataResponse()
│ ├── network.SendSuccessMessageResponse()
│ └── Include requested data
├── Client Error Response (400-499)
│ ├── network.SendBadRequestError() - Validation errors
│ ├── network.SendUnauthorizedError() - Auth failures
│ ├── network.SendForbiddenError() - Permission denied
│ └── network.SendNotFoundError() - Resource not found
└── Server Error Response (500-599)
├── network.SendInternalServerError() - System errors
└── network.SendMixedError() - Auto-detect error typeArchitectural Principles
1. Feature Independence
Each API feature (auth, blog, user) is completely independent:
- Separate database tables and relationships
- Isolated business logic and validation rules
- Independent deployment and scaling capabilities
- Clear API boundaries and contracts
2. Service Sharing Architecture
While features are independent, they share common services:
- Authentication Service - Shared across all features
- User Service - Referenced by blog service for author information
- Database Connection - Shared PostgreSQL connection pool
- Redis Cache - Shared caching infrastructure
3. API Key + JWT Dual Authentication
The system supports two authentication methods:
- API Keys - For external services and system integration
- JWT Tokens - For user authentication with RSA signing
4. Role-Based Authorization
Hierarchical permission system:
- Admin - Full system access
- Author - Can create and manage own blogs
- Editor - Can edit and publish blogs
- User - Basic user operations
5. Cache-Aside Pattern
Intelligent caching strategy:
- Read Operations - Check cache first, fallback to database
- Write Operations - Update database, invalidate cache
- Automatic Expiration - Time-based cache invalidation
Layer Responsibilities
1. Controllers
Location: api/[feature]/controller.go
Purpose: Handle HTTP requests and responses with authentication/authorization
Responsibilities:
- Define route endpoints within feature groups
- Parse and validate requests using DTOs
- Call service methods with proper error handling
- Format responses with consistent structure
- Handle HTTP-specific concerns (headers, status codes)
Controller Structure:
type controller struct {
network.Controller // Base controller with auth providers
common.ContextPayload // User context management
service Service // Business logic service
}
func NewController(
authProvider network.AuthenticationProvider,
authorizeProvider network.AuthorizationProvider,
service Service,
) network.Controller {
return &controller{
Controller: network.NewController("/blog", authProvider, authorizeProvider),
service: service,
}
}Route Mounting with Middleware:
func (c *controller) MountRoutes(group *gin.RouterGroup) {
// Public routes (no authentication required)
group.GET("/public", c.getPublicBlogs)
// Protected routes (authentication required)
protected := group.Group("/")
protected.Use(c.AuthProvider.Middleware())
{
// Author routes (author or admin role required)
author := protected.Group("/author")
author.Use(c.AuthorizeProvider.RequireRole("author", "admin"))
{
author.POST("/", c.createBlog)
author.GET("/my-blogs", c.getMyBlogs)
author.PUT("/id/:id", c.updateBlog)
author.DELETE("/id/:id", c.deleteBlog)
}
// Editor routes (editor or admin role required)
editor := protected.Group("/editor")
editor.Use(c.AuthorizeProvider.RequireRole("editor", "admin"))
{
editor.PUT("/id/:id/publish", c.publishBlog)
editor.GET("/drafts", c.getDraftBlogs)
}
}
}Request Handler Pattern:
func (c *controller) createBlog(ctx *gin.Context) {
// Parse and validate request body
body, err := network.ReqBody[dto.BlogCreate](ctx)
if err != nil {
network.SendBadRequestError(ctx, "Invalid request body", err)
return
}
// Get authenticated user from context
user := c.GetUser(ctx)
if user == nil {
network.SendUnauthorizedError(ctx, "User not authenticated", nil)
return
}
// Call service with user context
result, err := c.service.CreateBlog(body, user.ID)
if err != nil {
network.SendMixedError(ctx, err)
return
}
network.SendSuccessDataResponse(ctx, "Blog created successfully", result)
}
func (c *controller) getBlogByID(ctx *gin.Context) {
// Parse path parameter
idStr := ctx.Param("id")
id, err := uuid.Parse(idStr)
if err != nil {
network.SendBadRequestError(ctx, "Invalid blog ID format", err)
return
}
// Parse query parameters
includeAuthor := ctx.DefaultQuery("includeAuthor", "false") == "true"
// Call service
result, err := c.service.GetBlogByID(id, includeAuthor)
if err != nil {
network.SendMixedError(ctx, err)
return
}
network.SendSuccessDataResponse(ctx, "Blog retrieved successfully", result)
}2. Services
Location: api/[feature]/service.go
Purpose: Implement business logic with database operations and caching
Responsibilities:
- Business rule enforcement and validation
- Database CRUD operations with transactions
- Redis caching (cache-aside pattern)
- Data transformation between models and DTOs
- Cross-service communication logic
Service Interface Pattern:
type Service interface {
// Blog CRUD operations
CreateBlog(dto *dto.BlogCreate, authorID uuid.UUID) (*dto.BlogPrivate, error)
GetBlogByID(id uuid.UUID, includeAuthor bool) (*dto.BlogPublic, error)
UpdateBlog(id uuid.UUID, dto *dto.BlogUpdate, userID uuid.UUID) (*dto.BlogPrivate, error)
DeleteBlog(id uuid.UUID, userID uuid.UUID) error
// Blog listing with pagination
GetPublishedBlogs(query *dto.BlogQuery) (*dto.PaginatedBlogs, error)
GetAuthorBlogs(authorID uuid.UUID, page, limit int) (*dto.PaginatedBlogs, error)
// Blog management operations
PublishBlog(id uuid.UUID, userID uuid.UUID) (*dto.BlogPublic, error)
UpdateBlogSlug(id uuid.UUID, newSlug string, userID uuid.UUID) error
}Service Implementation with Caching:
type service struct {
db *pgxpool.Pool
cache redis.Cache[dto.BlogCache]
userService user.Service // Dependency on user service
}
func NewService(db *pgxpool.Pool, store redis.Store, userService user.Service) Service {
return &service{
db: db,
cache: redis.NewCache[dto.BlogCache](store),
userService: userService,
}
}
func (s *service) GetBlogByID(id uuid.UUID, includeAuthor bool) (*dto.BlogPublic, error) {
// Try cache first (cache-aside pattern)
cached, err := s.cache.Get(id.String())
if err == nil && cached != nil {
blog := s.convertCacheToDTO(cached)
if includeAuthor {
author, err := s.userService.FetchUserById(cached.AuthorID)
if err == nil {
blog.Author = &dto.UserInfo{
ID: author.ID,
Name: author.Name,
}
}
}
return blog, nil
}
// Cache miss - query database
blog, err := s.getBlogFromDatabase(id, includeAuthor)
if err != nil {
return nil, err
}
// Update cache (don't include author info in cache)
cacheData := s.convertBlogToCache(blog)
s.cache.Set(id.String(), cacheData, time.Hour)
return blog, nil
}
func (s *service) CreateBlog(dto *dto.BlogCreate, authorID uuid.UUID) (*dto.BlogPrivate, error) {
// Business rule validation
if err := s.validateBlogCreate(dto, authorID); err != nil {
return nil, err
}
// Check slug uniqueness
if exists, _ := s.blogSlugExists(dto.Slug); exists {
return nil, network.NewBadRequestError("Blog with this slug already exists", nil)
}
// Start transaction
tx, err := s.db.Begin(context.Background())
if err != nil {
return nil, network.NewInternalServerError("Failed to start transaction", err)
}
defer tx.Rollback(context.Background())
// Create blog in database
blog, err := s.createBlogInTransaction(tx, dto, authorID)
if err != nil {
return nil, err
}
// Commit transaction
if err = tx.Commit(context.Background()); err != nil {
return nil, network.NewInternalServerError("Failed to commit transaction", err)
}
return blog, nil
}
func (s *service) PublishBlog(id uuid.UUID, userID uuid.UUID) (*dto.BlogPublic, error) {
// Check permissions (author or editor/admin)
user, err := s.userService.FetchUserById(userID)
if err != nil {
return nil, network.NewUnauthorizedError("User not found", err)
}
if !s.canPublishBlog(user) {
return nil, network.NewForbiddenError("Insufficient permissions to publish blog", nil)
}
// Update blog status
blog, err := s.updateBlogStatus(id, true, userID)
if err != nil {
return nil, err
}
// Invalidate cache
s.cache.Delete(id.String())
// Publish event for system integration
// s.publishBlogPublishedEvent(blog)
return s.convertToPublicDTO(blog), nil
}Business Logic Validation:
func (s *service) validateBlogCreate(dto *dto.BlogCreate, authorID uuid.UUID) error {
// Check author permissions
user, err := s.userService.FetchUserById(authorID)
if err != nil {
return network.NewUnauthorizedError("Author not found", err)
}
if user.Role != "author" && user.Role != "admin" {
return network.NewForbiddenError("Only authors can create blogs", nil)
}
// Validate slug format
if !s.isValidSlug(dto.Slug) {
return network.NewBadRequestError("Invalid slug format", nil)
}
// Check tag limits
if len(dto.Tags) > 10 {
return network.NewBadRequestError("Too many tags (max 10)", nil)
}
return nil
}3. Models
Location: api/[feature]/model/[entity].go
Purpose: Define database schema
Responsibilities:
- Represent database tables
- Define data types
- Document field mappings
Example:
type Blog struct {
ID uuid.UUID // id
Title string // title
Description string // description
Text *string // text
DraftText string // draft_text
Tags []string // tags
AuthorID uuid.UUID // author_id
Slug string // slug
Status bool // status
Published bool // published
CreatedAt time.Time // created_at
UpdatedAt time.Time // updated_at
}4. DTOs (Data Transfer Objects)
Location: api/[feature]/dto/[operation].go
Purpose: Define request/response schemas
Responsibilities:
- Input validation
- Output formatting
- Type safety
- API contract documentation
Example:
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"`
}
type BlogPublic struct {
ID uuid.UUID `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Slug string `json:"slug"`
Author *UserInfo `json:"author"`
Tags []string `json:"tags"`
PublishedAt time.Time `json:"publishedAt"`
}5. Middleware
Location: api/[feature]/middleware/[name].go
Purpose: Process requests before/after handlers
Responsibilities:
- Authentication
- Authorization
- Request validation
- Logging
- Rate limiting
Example:
type authenticationProvider struct {
common.ContextPayload
authService auth.Service
userService user.Service
}
func (m *authenticationProvider) Middleware() gin.HandlerFunc {
return func(ctx *gin.Context) {
token := utils.ExtractBearerToken(ctx.GetHeader("Authorization"))
claims, err := m.authService.VerifyToken(token)
if err != nil {
network.SendUnauthorizedError(ctx, err.Error(), err)
return
}
user, err := m.userService.FetchUserById(claims.Subject)
if err != nil {
network.SendUnauthorizedError(ctx, "User not found", err)
return
}
m.SetUser(ctx, user)
ctx.Next()
}
}Module System
Module Interface
The startup/module.go file implements dependency injection:
type Module interface {
GetInstance() *module
Controllers() []network.Controller
RootMiddlewares() []network.RootMiddleware
AuthenticationProvider() network.AuthenticationProvider
AuthorizationProvider() network.AuthorizationProvider
}
type module struct {
Context context.Context
Env *config.Env
DB postgres.Database
Store redis.Store
UserService user.Service
AuthService auth.Service
BlogService blog.Service
}Dependency Wiring
func NewModule(ctx context.Context, env *config.Env, db postgres.Database, store redis.Store) Module {
// Initialize services with dependencies
userService := user.NewService(db.Pool())
authService := auth.NewService(db.Pool(), env, userService)
blogService := blog.NewService(db.Pool(), store, userService)
return &module{
Context: ctx,
Env: env,
DB: db,
Store: store,
UserService: userService,
AuthService: authService,
BlogService: blogService,
}
}
func (m *module) Controllers() []network.Controller {
return []network.Controller{
auth.NewController(m.AuthenticationProvider(), m.AuthorizationProvider(), m.AuthService),
user.NewController(m.AuthenticationProvider(), m.AuthorizationProvider(), m.UserService),
blog.NewController(m.AuthenticationProvider(), m.AuthorizationProvider(), m.BlogService),
author.NewController(m.AuthenticationProvider(), m.AuthorizationProvider(), author.NewService(m.DB.Pool(), m.BlogService)),
editor.NewController(m.AuthenticationProvider(), m.AuthorizationProvider(), editor.NewService(m.DB.Pool(), m.UserService)),
blogs.NewController(m.AuthenticationProvider(), m.AuthorizationProvider(), blogs.NewService(m.DB.Pool(), m.Store)),
contact.NewController(m.AuthenticationProvider(), m.AuthorizationProvider(), contact.NewService(m.DB.Pool())),
}
}Feature Organization
Independent Features
Features with separate endpoints get their own directories:
api/
├── auth/ # /auth endpoints
├── user/ # /profile endpoints
├── blogs/ # /blogs endpoints (public listing)
└── contact/ # /contact endpointsShared Resources
Related features share a directory:
api/blog/
├── author/ # /blog/author endpoints
├── editor/ # /blog/editor endpoints
├── dto/ # Shared DTOs
├── model/ # Shared models
├── controller.go # /blog endpoints
└── service.go # Shared serviceDatabase Access
Connection Pooling
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, dbConfig)
db.Connect()Query Patterns
// Single row query
var user model.User
err := db.QueryRow(ctx, query, id).Scan(&user.ID, &user.Name, &user.Email)
// Multiple rows
rows, err := db.Query(ctx, query, params...)
defer rows.Close()
for rows.Next() {
var item model.Item
rows.Scan(&item.ID, &item.Name)
items = append(items, &item)
}
// Insert with RETURNING
err := db.QueryRow(ctx, insertQuery, values...).Scan(&id, &createdAt)
// Transactions
tx, err := db.Begin(ctx)
defer tx.Rollback(ctx)
// ... perform operations
tx.Commit(ctx)Caching Strategy
Redis Integration
type service struct {
db *pgxpool.Pool
cache redis.Cache[dto.BlogCache]
}
func NewService(db *pgxpool.Pool, store redis.Store) Service {
return &service{
db: db,
cache: redis.NewCache[dto.BlogCache](store),
}
}Cache Patterns
// Cache-aside pattern
func (s *service) GetBlogById(id uuid.UUID) (*dto.BlogPublic, error) {
// Try cache first
blog, err := s.GetBlogDtoCacheById(id)
if err == nil {
return blog, nil
}
// Query database
blog, err = s.GetPublishedBlogById(id)
if err != nil {
return nil, err
}
// Update cache
s.SetBlogDtoCacheById(blog)
return blog, nil
}Error Handling
Error Types
// Client errors (400-499)
network.SendBadRequestError(ctx, "Invalid input", err)
network.SendUnauthorizedError(ctx, "Authentication required", err)
network.SendForbiddenError(ctx, "Permission denied", err)
network.SendNotFoundError(ctx, "Resource not found", err)
// Server errors (500-599)
network.SendInternalServerError(ctx, "Something went wrong", err)
// Mixed (auto-detect from error type)
network.SendMixedError(ctx, err)Custom Errors
// In service layer
if exists {
return nil, network.NewBadRequestError("Resource already exists", nil)
}
if !authorized {
return nil, network.NewForbiddenError("Insufficient permissions", nil)
}Testing Architecture
Unit Tests
Test individual functions with mocks:
func TestAuthController_SignupSuccess(t *testing.T) {
mockService := new(auth.MockService)
mockService.On("SignUpBasic", mock.Anything).Return(&dto.UserAuth{}, nil)
controller := auth.NewController(nil, nil, mockService)
// Test controller method
}Integration Tests
Test complete request/response cycles:
func TestIntegration_CreateBlog(t *testing.T) {
router, module, teardown := startup.TestServer()
defer teardown()
// Create user and get token
token := createTestUser(t, router)
// Make authenticated request
response := makeRequest(t, router, "POST", "/blog/author", token, body)
assert.Equal(t, 200, response.StatusCode)
}Next Steps
- Understand Core Concepts in depth
- Learn about Configuration options
- Explore API Reference for complete documentation
