Building Scalable REST APIs with Go and Clean Architecture

API Design
Backend
Clean Architecture
Go
31 Desember 2025

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:

  • Easier testing and mocking
  • Independence from frameworks and databases
  • Clear separation of business logic
  • Better team collaboration
  • 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: