Building Scalable REST APIs with Go and Clean Architecture
As a backend developer working with Go at HMDTIF FILKOM UB, I've learned that building scalable REST APIs isn't just about writing code that works—it's about writing code that lasts. In this article, I'll share my experience implementing Clean Architecture in a real-world Go project.
Why Clean Architecture?
When I first started building the HMDTIF backend system, I faced a common challenge: how to structure the codebase so it remains maintainable as features grow. Clean Architecture, popularized by Robert C. Martin, provides a solution by separating concerns into distinct layers.
The key benefits I've experienced:
Project Structure
Here's the structure I use for Go projects:
project/
├── cmd/
│ └── main.go
├── internal/
│ ├── domain/ # Business entities
│ ├── usecase/ # Business logic
│ ├── repository/ # Data access
│ └── handler/ # HTTP handlers
├── pkg/
│ ├── middleware/
│ └── response/
└── config/
The Domain Layer
The domain layer contains your core business entities. These are pure Go structs with no external dependencies:
type News struct {
ID int `json:"id"`
Title string `json:"title"`
Slug string `json:"slug"`
Content string `json:"content"`
AuthorID int `json:"author_id"`
IsFeatured bool `json:"is_featured"`
PublishedAt time.Time `json:"published_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
The Repository Layer
Repositories handle data persistence. I use interfaces to keep the code testable:
type NewsRepository interface {
Create(news *News) error
GetByID(id int) (*News, error)
GetBySlug(slug string) (*News, error)
Update(news *News) error
Delete(id int) error
List(filter NewsFilter) ([]*News, error)
}
The Use Case Layer
This is where business logic lives. Use cases orchestrate the flow of data:
type NewsUseCase struct {
repo NewsRepository
}
func (uc *NewsUseCase) CreateNews(input CreateNewsInput) (*News, error) {
// Validate input
if err := input.Validate(); err != nil {
return nil, err
}
// Generate slug
slug := generateSlug(input.Title)
// Create news entity
news := &News{
Title: input.Title,
Slug: slug,
Content: input.Content,
AuthorID: input.AuthorID,
}
// Save to database
if err := uc.repo.Create(news); err != nil {
return nil, err
}
return news, nil
}
Key Lessons Learned
After implementing 8+ CRUD endpoints for the HMDTIF system, here are my key takeaways:
1. Start with interfaces
Define your repository and use case interfaces first. This makes testing much easier.
2. Keep domain entities pure
Don't let database concerns leak into your domain models. Use separate structs for database operations if needed.
3. Use middleware wisely
Authentication, logging, and error handling are perfect candidates for middleware.
Conclusion
Clean Architecture has significantly improved the maintainability of my Go projects. While it requires more initial setup, the long-term benefits are worth it—especially when working in teams or maintaining code over time.
Resources: