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 &registry{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 in pkg/usecase/repository/ abstract persistence.
  • Concrete implementations belong in pkg/adapter/repository/ (data access) and pkg/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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →