# How to Add New REST Endpoints in a Clean Architecture Go Project

> Learn to add new REST endpoints in the manakuro golang clean architecture project. Define domain models, implement use cases, and register services for efficient API development.

- Repository: [manato/golang-clean-architecture](https://github.com/manakuro/golang-clean-architecture)
- Tags: how-to-guide
- Published: 2026-03-06

---

**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`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/domain/model/article.go)

```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`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/usecase/usecase/article.go)

```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`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/usecase/repository/article.go)

```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`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/adapter/repository/article.go)

```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`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/registry/registry.go)

```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`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/adapter/controller/article.go)

```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`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/infrastructure/router/router.go)

```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`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/adapter/controller/app.go)

```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`](https://github.com/manakuro/golang-clean-architecture/blob/main/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:

```bash
go run cmd/app/main.go

```

Test the new endpoint:

```bash
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`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/registry/registry.go), wiring repositories to use-cases.
- **Routing** occurs in [`pkg/infrastructure/router/router.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/infrastructure/router/router.go), mapping Echo paths to controller methods.
- **AppController** aggregates all controllers in [`pkg/adapter/controller/app.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/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.