In this article, we are going to apply the principles of the clean architecture by (Uncle) Bob Martin in our Golang API. But first let’s do a quick overview of those principles in order to follow a clean architecture approach in our application:

  • Independent of frameworks.
  • Testable.
  • Independent of UI.
  • Independent of Database.
  • Independent of any external agency.

The application should be independent of frameworks, so we are not tied to a specific framework. Our application should be able replace the HTTP framework , example from Mux to Chi. Our application should be testable so we can test the business logic without the database or the UI. We need to be independent of the UI in our case we are working on a Restful API so we don’t care if the client is a web browser or a mobile application or an IOT device. Our application should be also independent of the database so we should be able to change the persistence layer, we can start using Firestore and then we can replace it by MySQL or MongoDB for example. And our application should be also independent of any external agency, so our business rules don’t know anything about any external factors.

Repository layer

In previous article, we created a repository to connect our application to the Firestore database. In our repository, we created an interface and we created a couple of methods, one to save the posts and another one to get all the existing posts from the Firestore database.

Having this interface will allow us to keep independent the concrete implementation from the rest of the application. So we keep the interface in the file posts-repo.go as follow:

package repository

import "golang-mux-api/entity"

type PostRepository interface {
	Save(post *entity.Post) (*entity.Post, error)
	FindAll() ([]entity.Post, error)
}

We are going to create a new file named firestore-repo.go that contains the implementation the interface PostRepository in case of Firestore database:

package repository

import (
	"context"
	"golang-mux-api/entity"
	"log"
	"cloud.google.com/go/firestore"
	"google.golang.org/api/iterator"
)

type repo struct{}

// NewFirestoreRepository creates a new repo
func NewFirestoreRepository() PostRepository {
	return &repo{}
}

const (
	projectId      string = "pragmatic-reviews-62551"
	collectionName string = "posts"
)

func (*repo) Save(post *entity.Post) (*entity.Post, error) {
	ctx := context.Background()
	client, err := firestore.NewClient(ctx, projectId)
	if err != nil {
		log.Fatalf("Failed to create a Firestore Client: %v", err)
		return nil, err
	}

	defer client.Close()
	_, _, err = client.Collection(collectionName).Add(ctx, map[string]interface{}{
		"ID":    post.ID,
		"Title": post.Title,
		"Text":  post.Text,
	})

	if err != nil {
		log.Fatalf("Failed adding a new post: %v", err)
		return nil, err
	}

	return post, nil
}

func (*repo) FindAll() ([]entity.Post, error) {
	ctx := context.Background()
	client, err := firestore.NewClient(ctx, projectId)
	if err != nil {
		log.Fatalf("Failed to create a Firestore Client: %v", err)
		return nil, err
	}
	defer client.Close()
	var posts []entity.Post
	iter := client.Collection(collectionName).Documents(ctx)

	for {
		doc, err := iter.Next()
		
		if err == iterator.Done {
			break
		}
		if err != nil {
			log.Fatalf("Failed to iterate the list of posts: %v", err)
		}
		post := entity.Post{
			ID:    doc.Data()["ID"].(int64),
			Title: doc.Data()["Title"].(string),
			Text:  doc.Data()["Text"].(string),
		}
		posts = append(posts, post)
	}
	return posts, nil
}

And we can create more some blank repo files, for example repository/mysql-repo.go for MySQL repository implementation or repository/mongo-repo.go for MongoDB repository implementation

Service layer

If we go to route.go file, we can se that we have mixed up some business logic with code that is specific for a controller like setting content type header or writting the response or encoding a specific object. So we are going to split this in a service layer. Service layer is going to be the use case or the business logic, and then the controller is going to be the code that is responsible of interacting with the API layer.

We are going to create a new folder called service and another one called controller. Then our service is going to be called from the controller, and the service is going to call the repository to add new posts and get existing posts from the database.

We are going to create a new fire called post-service.go that contains the interface for the service type PostService and it has three methods: one for validating the post, one for creating new post, and one for finding all posts.

And we can pass the repository with type PostRepository as parameter of contructor, this make our service is independent of database.

package service

import (
	"errors"
	"golang-mux-api/entity"
	"golang-mux-api/repository"
	"math/rand"
)

type PostService interface {
	Validate(post *entity.Post) error
	Create(post *entity.Post) (*entity.Post, error)
	FindAll() ([]entity.Post, error)
}

type service struct{}

var (
	repo repository.PostRepository
)

func NewPostService(postRepository repository.PostRepository) PostService {
	repo = postRepository
	return &service{}
}

func (*service) Validate(post *entity.Post) error {
	if post == nil {
		err := errors.New("The post is empty")
		return err
	}

	if post.Title == "" {
		err := errors.New("The post title is empty")
		return err
	}
	return nil
}

func (*service) Create(post *entity.Post) (*entity.Post, error) {
	post.ID = rand.Int63()
	return repo.Save(post)
}

func (*service) FindAll() ([]entity.Post, error) {
	return repo.FindAll()
}

Controller layer

We are going to create new file called post-controller.go will use the PostService we created above as service parameter on the constructor.

package controller

import (
	"encoding/json"
	"golang-mux-api/entity"
	"golang-mux-api/errors"
	"golang-mux-api/service"
	"net/http"
)

type controller struct{}

var (
	postService service.PostService
)

type PostController interface {
	GetPosts(response http.ResponseWriter, request *http.Request)
	AddPost(response http.ResponseWriter, request *http.Request)
}

// More effiction way to use Dependency Injection
func NewPostController(service service.PostService) PostController {
	postService = service
	return &controller{}
}

func (*controller) GetPosts(response http.ResponseWriter, request *http.Request) {
	response.Header().Set("Content-type", "application/json")
	posts, err := postService.FindAll()
	if err != nil {
		response.WriteHeader(http.StatusInternalServerError)
		json.NewEncoder(response).Encode(errors.ServiceError{Message: "Error getting the posts"})
	}
	response.WriteHeader(http.StatusOK)
	json.NewEncoder(response).Encode(posts)
}

func (*controller) AddPost(response http.ResponseWriter, request *http.Request) {
	response.Header().Set("Content-type", "application/json")
	var post entity.Post
	err := json.NewDecoder(request.Body).Decode(&post)
	if err != nil {
		response.WriteHeader(http.StatusInternalServerError)
		json.NewEncoder(response).Encode(errors.ServiceError{Message: "Error unmarshalling data"})
		return
	}
	err1 := postService.Validate(&post)
	if err1 != nil {
		response.WriteHeader(http.StatusInternalServerError)
		json.NewEncoder(response).Encode(errors.ServiceError{Message: err1.Error()})
		return
	}

	result, err2 := postService.Create(&post)
	if err2 != nil {
		response.WriteHeader(http.StatusInternalServerError)
		json.NewEncoder(response).Encode(errors.ServiceError{Message: "Error saving the post"})
		return
	}
	response.WriteHeader(http.StatusOK)
	json.NewEncoder(response).Encode(result)
}

HTTP / API Layer

Another thing that we need to change in order to be independent of frameworks here we depend on Mux so the implementation of our API depends on Mux. So we need to create a layer if tomorrow we want to use a specific HTTP server or if we want to implement our own HTTP framework we should be able to do it.

We are going to create a router interface that is going to implement the GET method and the POST method. If we add new operations in our API to handle all the HTTP verbs or methods, we are gonna need to add the PUT method, DELETE method, PATCH method, etc.

We are going to create new folder called http and we are going to create a new file and this is going to be the interface called router.go

package router

import "net/http"

type Router interface {
	GET(uri string, f func(resp http.ResponseWriter, request *http.Request))
	POST(uri string, f func(resp http.ResponseWriter, request *http.Request))
	SERVE(port string)
}

Now we have our router interface, let’s create concrete implementation for the Mux framework, so we are going to create new file called mux-router.go

package router

import (
	"fmt"
	"net/http"
	"github.com/gorilla/mux"
)

type muxRouter struct{}

var (
	muxDispatcher = mux.NewRouter()
)

func NewMuxRouter() Router {
	return &muxRouter{}
}

func (*muxRouter) GET(uri string, f func(resp http.ResponseWriter, request *http.Request)) {
	muxDispatcher.HandleFunc(uri, f).Methods("GET")
}

func (*muxRouter) POST(uri string, f func(resp http.ResponseWriter, request *http.Request)) {
	muxDispatcher.HandleFunc(uri, f).Methods("POST")
}

func (*muxRouter) SERVE(port string) {
	fmt.Printf("Mux HTTP server running on port %v", port)
	http.ListenAndServe(port, muxDispatcher)
}

Mux is just one possible implementation of this router interface and we can also use any other HTTP framework such as Chi . We are going to implement another router using that specific framework, so we are going to create a new file called chi-router.go . First, we need to install this Chi library:

$ go get -u github.com/go-chi/chi

And here is the chi-router.go implementation

package router

import (
	"fmt"
	"net/http"
	"github.com/go-chi/chi"
)

type chaiRouter struct{}

var (
	chiDispatcher = chi.NewRouter()
)

func NewChiRouter() Router {
	return &chaiRouter{}
}

func (*chaiRouter) GET(uri string, f func(resp http.ResponseWriter, request *http.Request)) {
	chiDispatcher.Get(uri, f)
}

func (*chaiRouter) POST(uri string, f func(resp http.ResponseWriter, request *http.Request)) {
	chiDispatcher.Post(uri, f)
}

func (*chaiRouter) SERVE(port string) {
	fmt.Printf("Chi HTTP server running on port %v", port)
	http.ListenAndServe(port, chiDispatcher)
}

Server Application

Now we have all interfaces and implementations for Repository, Service, Controller, and HTTP framwork. We are going to pass the service to the controller …, a more efficient way to do this is going to be using some kind of dependency injection framework. Let’s delete the file route.go and make change for the file server.go as follow:

package main

import (
	"fmt"
	"golang-rest-api/controller"
	router "golang-rest-api/http"
	"golang-rest-api/repository"
	"golang-rest-api/service"
	"net/http"
)

var (
	postRepository repository.PostRepository = repository.NewFirestoreRepository()
	postService    service.PostService       = service.NewPostService(postRepository)
	postController controller.PostController = controller.NewPostController(postService)
	//httpRouter     router.Router             = router.NewMuxRouter()
	httpRouter router.Router = router.NewChiRouter()
)

func main() {
	const port string = ":8000"

	httpRouter.GET("/", func(resp http.ResponseWriter, request *http.Request) {
		fmt.Fprint(resp, "Up and running...")
	})

	httpRouter.GET("/posts", postController.GetPosts)
	httpRouter.POST("/posts", postController.AddPost)

	httpRouter.SERVE(port)
}

Run and test REST APIs

Start our REST API

$ go build
$ go run .

Let’s create more Post

$ curl -v --location 'http://localhost:8000/posts' --header 'Content-Type: application/json' --data '{
    "title": "Title 3",
    "text": "Text 3"
}'

Then get all posts

$ curl -v --location 'http://localhost:8000/posts'

Download Source Code

$ git clone https://github.com/favtuts/golang-mux-api.git
$ cd golang-mux-api

$ git checkout clean-architecture
$ go build

$ go run .

Leave a Reply

Your email address will not be published. Required fields are marked *