How to Add New REST Endpoints in a Clean Architecture Go Project
To add a new REST endpoint in the manakuro/golang-clean-architecture project, you must define a domain model, create use-case and repository interfaces with their implementations, register them in the service registry, implement a controller method, and finally add the route in the router.
The manakuro/golang-clean-architecture repository demonstrates Clean Architecture principles by strictly separating domain logic, use cases, and infrastructure concerns. Adding new REST endpoints requires touching multiple layers—from domain models to HTTP routing—but following this pattern ensures your code remains testable, maintainable, and independent of frameworks.
Step 1: Define the Domain Model
Create a struct that represents the core entity your endpoint will manipulate. This belongs in the domain layer and should not import infrastructure concerns.
File: pkg/domain/model/article.go
package model
type Article struct {
ID int64 `json:"id"`
Title string `json:"title"`
Content string `json:"content"`
}
Step 2: Create the Use-Case Interface and Implementation
The use-case layer defines the business logic contract. Declare an interface in pkg/usecase/usecase/ and provide a concrete implementation that orchestrates domain operations.
File: pkg/usecase/usecase/article.go
package usecase
import (
"golang-clean-architecture/pkg/domain/model"
"golang-clean-architecture/pkg/usecase/repository"
)
type Article interface {
GetByID(id int64) (*model.Article, error)
}
type articleUsecase struct {
repo repository.Article
}
func NewArticleUsecase(r repository.Article) Article {
return &articleUsecase{repo: r}
}
func (a *articleUsecase) GetByID(id int64) (*model.Article, error) {
return a.repo.FindByID(id)
}
Step 3: Add the Repository Interface and Implementation
Define the persistence contract in the use-case layer (pkg/usecase/repository/) to keep business logic decoupled from database details. Implement the actual data access in the adapter layer (pkg/adapter/repository/).
Interface file: pkg/usecase/repository/article.go
package repository
import "golang-clean-architecture/pkg/domain/model"
type Article interface {
FindByID(id int64) (*model.Article, error)
}
Implementation file: pkg/adapter/repository/article.go
package repository
import (
"database/sql"
"golang-clean-architecture/pkg/domain/model"
)
type articleRepo struct {
db *sql.DB
}
func NewArticleRepo(db *sql.DB) Article {
return &articleRepo{db: db}
}
func (r *articleRepo) FindByID(id int64) (*model.Article, error) {
var a model.Article
err := r.db.QueryRow("SELECT id, title, content FROM articles WHERE id = ?", id).
Scan(&a.ID, &a.Title, &a.Content)
if err != nil {
return nil, err
}
return &a, nil
}
Step 4: Register Dependencies in the Service Registry
Wire the concrete repository and use-case implementations together in the registry so they can be injected into controllers.
File: pkg/registry/registry.go
package registry
import (
"database/sql"
"golang-clean-architecture/pkg/adapter/repository"
"golang-clean-architecture/pkg/usecase/usecase"
)
type registry struct {
db *sql.DB
}
type Registry interface {
UserUsecase() usecase.User
ArticleUsecase() usecase.Article // new method
}
func NewRegistry(db *sql.DB) Registry {
return ®istry{db: db}
}
func (r *registry) ArticleUsecase() usecase.Article {
articleRepo := repository.NewArticleRepo(r.db)
return usecase.NewArticleUsecase(articleRepo)
}
Step 5: Implement the Controller Method
Create a controller that translates HTTP requests into use-case calls and formats responses. This belongs in pkg/adapter/controller/.
File: pkg/adapter/controller/article.go
package controller
import (
"net/http"
"strconv"
"golang-clean-architecture/pkg/usecase"
"github.com/labstack/echo/v4"
)
type Context interface {
Param(name string) string
JSON(code int, i interface{}) error
}
type articleController struct {
uc usecase.Article
}
type Article interface {
GetByID(c Context) error
}
func NewArticleController(uc usecase.Article) Article {
return &articleController{uc: uc}
}
func (ac *articleController) GetByID(ctx Context) error {
idStr := ctx.Param("id")
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
return ctx.JSON(http.StatusBadRequest, map[string]string{"error": "invalid id"})
}
article, err := ac.uc.GetByID(id)
if err != nil {
return ctx.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return ctx.JSON(http.StatusOK, article)
}
Step 6: Add the Route to the Router
Expose the endpoint by mapping an HTTP path and verb to the controller method in the infrastructure layer.
File: pkg/infrastructure/router/router.go
package router
import (
"golang-clean-architecture/pkg/adapter/controller"
"github.com/labstack/echo/v4"
)
func NewRouter(e *echo.Echo, cApp controller.AppController) {
e.GET("/users", func(c echo.Context) error { return cApp.User.GetUsers(c) })
e.POST("/users", func(c echo.Context) error { return cApp.User.CreateUser(c) })
// New endpoint
e.GET("/articles/:id", func(c echo.Context) error { return cApp.Article.GetByID(c) })
}
Step 7: Update the AppController
Aggregate the new controller into the main application controller struct so it can be passed to the router.
File: pkg/adapter/controller/app.go
package controller
type AppController struct {
User User
Article Article // new field
}
func NewAppController(r registry.Registry) *AppController {
return &AppController{
User: NewUserController(r.UserUsecase()),
Article: NewArticleController(r.ArticleUsecase()), // new line
}
}
Step 8: Bootstrap and Test
The application entry point at cmd/app/main.go initializes the Echo server, builds the registry, creates the AppController, and passes it to the router. No changes are required here unless you are adding a fundamentally new controller category.
Run the server:
go run cmd/app/main.go
Test the new endpoint:
curl http://localhost:8080/articles/42
Summary
- Domain models live in
pkg/domain/model/and define core entities without external dependencies. - Use-case interfaces in
pkg/usecase/usecase/define business logic contracts, while repository interfaces inpkg/usecase/repository/abstract persistence. - Concrete implementations belong in
pkg/adapter/repository/(data access) andpkg/adapter/controller/(HTTP handling). - Dependency injection is centralized in
pkg/registry/registry.go, wiring repositories to use-cases. - Routing occurs in
pkg/infrastructure/router/router.go, mapping Echo paths to controller methods. - AppController aggregates all controllers in
pkg/adapter/controller/app.go, acting as the single entry point for the router.
Frequently Asked Questions
Where should I place input validation logic when adding a new endpoint?
Input validation should occur in the controller layer (pkg/adapter/controller/). Parse and validate request parameters using the Echo context (e.g., ctx.Param, ctx.Bind) before passing sanitized data to the use-case. This keeps HTTP concerns out of your business logic.
Do I need to create both use-case and repository interfaces for every new endpoint?
Yes, following the Dependency Inversion Principle as implemented in this project. Define the repository interface in pkg/usecase/repository/ so the use-case depends on an abstraction, not concrete database code. Implement the actual SQL or ORM logic in pkg/adapter/repository/.
How do I handle database transactions across multiple repositories?
Create a unit of work or transaction manager interface in pkg/usecase/repository/ that abstracts transaction boundaries. Implement this in pkg/adapter/repository/ using your database driver's transaction capabilities. The use-case can then orchestrate multiple repository calls within a single transaction without knowing the underlying implementation details.
Can I reuse existing domain models for new endpoints?
Yes, if the new endpoint operates on existing entities. Simply reference the existing struct from pkg/domain/model/ in your use-case and controller. Only create new domain models when introducing distinct business entities that don't already exist in the domain layer.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →