Develop an API with Go using dev container - 3

hero image Photo by Mohammad Rahmani on Unsplash

We’ve learn about some architectural pattern to build a scalable back-end service. Now we will implement the service using (Service / Repository Pattern) from the previous post.

Our goal is to create a phone validation service, It will have one API endpoint which takes a phone number as input. And the API will send whether the input phone number is valid. To make the API able to communicate to DB, we will also save the validation result into the DB we setup in part 1, and return the value from DB if user input the same phone number.

We will create a folder called pkg, and this folder will contains our service, controller, repository and data model. Now we will have the folder structure below

phone_validator
├─ .devcontainer
│ ├─ .env
│ ├─ Dockerfile
│ ├─ devcontainer.json
│ └─ docker-compose.yml
├─ .gitignore
├─ go.mod
├─ go.sum
├─ main.go
└─ pkg

Create the DB model for the data to store

First, we will define the data model for the validation result. We will create a db folder to contain our data models. In the db folder, let’s create a file called phoneNumber.go which will be responsible for create and provide the ability for gorm to manipulate the phoneNumber table in the DB.

package db

type PhoneNumber struct {

	PhoneNumber string `gorm:"primaryKey" json:"phone_number"`
	
	Valid bool
}

As you could see, we create a struct call PhoneNumber and specify the fields. We use gorm:"primaryKey" to assign the PhoneNumber field as primary key since we expect the same phone number will have the same validation result. And use json:"phone_number" to make the PhoneNumber field parse to phone_number when sending the JSON to client. Also, we have to store the validation result, so we specified a field named Valid with a boolean for it.

Design and implement the interface for repository layer

We have create our DB model, now let’s design the repository layer for our service. The repository layer is responsible for provide a abstraction between our service logic and database implementation. With repository layer the service layer just need to focus on the function input and the output with the repository, regardless the DB is SQL or non-SQL and what query command should be used.

For our service, we need to abstract on two DB implementation:

  1. create a validation result with the phoneNumber
  2. get a validation result of the phoneNumber

Inside the pkg folder, let’s create a folder called repositories. And then create a folder inside repositories folder, called entities. Then we will create a phoneNumber.go file inside the entities folder.

package entities

type PhoneNumber struct {

	PhoneNumber string
	
	Valid bool
	
	UpdatedAt int
}

The repository will convert the db models to the entities, and the entities will be used across the services. By doing this, we can get rid of the DB implementation and focus on the logic we care about.

Then we will create a phoneNumber.go file inside the repositories folder.

Once we create the file, we can start design our interface for the repository layer.

package repositories

import (
	"phone_validator/pkg/repositories/entities"
	
	"gorm.io/gorm"
)

type PhoneNumberRepository interface {

	// create a validation result with the phoneNumber
	CreatePhoneNumberResult(phoneNumber entities.PhoneNumber) (entities.PhoneNumber, error)
	
	// get a validation result of the phoneNumber
	GetPhoneNumberResult(phoneNumber string) (entities.PhoneNumber, error)
}

type phoneNumberRepository struct {
	db *gorm.DB
}

The CreatePhoneNumberResult should receive a phoneNumber model to create the validation result and it should return the model once created.

The GetPhoneNumberResult should receive a phone number to query the result from database.

Implement the repository

Now, we had finished the design of our repository, let’s implement the repository logic

package repositories

import (
	"phone_validator/pkg/repositories/entities"
	
	models "phone_validator/pkg/db"
	
	"gorm.io/gorm"
)


type PhoneNumberRepository interface {
	// The interface 
}

type phoneNumberRepository struct {
	// The struct
}

//Step 1. create a function to create the repository instance, and inject the DB into the instance
func NewPhoneNumberResultRepository(db *gorm.DB) PhoneNumberRepository {
	return &phoneNumberRepository{
	db: db,
	}
}

//Step 2: immplment the functions inside the PhoneNumberRepository interface
func (r *phoneNumberRepository) CreatePhoneNumberResult(phoneNumber entities.PhoneNumber) (entities.PhoneNumber, error) {
	//Map the PhoneNumber entity to PhoneNumber Db model
	phoneNumberToCreate := models.PhoneNumber{	
		PhoneNumber: phoneNumber.PhoneNumber,
		Valid: phoneNumber.Valid,
		UpdatedAt: phoneNumber.UpdatedAt,
	}

	// Save the PhoneNumber Db model
	err := r.db.Create(&phoneNumberToCreate).Error
		
	if err != nil {
		return phoneNumber, err
	}

	// Map PhoneNumber Db model to PhoneNumber entity
	var phoneNumberEntity entities.PhoneNumber
	phoneNumberEntity.PhoneNumber = phoneNumberToCreate.PhoneNumber
	phoneNumberEntity.Valid = phoneNumberToCreate.Valid
	phoneNumberEntity.UpdatedAt = phoneNumberToCreate.UpdatedAt
	
	return phoneNumberEntity, nil
}

func (r *phoneNumberRepository) GetPhoneNumberResult(phoneNumber string) (entities.PhoneNumber, error) {
	var phoneNumberRes models.PhoneNumber
	// Query the phone number
	r.db.First(&phoneNumberRes, phoneNumber)
	
	var phoneNumberEntity entities.PhoneNumber
	
	// Map PhoneNumber Db model to PhoneNumber entity
	phoneNumberEntity.PhoneNumber = phoneNumberRes.PhoneNumber
	phoneNumberEntity.Valid = phoneNumberRes.Valid
	phoneNumberEntity.UpdatedAt = phoneNumberRes.UpdatedAt
	
	return phoneNumberEntity, nil
}

Design and implement the interface for service layer

We’ve done the repository layer to retrieve and convert the data from DB to the entity. Next we will create the service layer. The service layer is a place for implement the business logic. it stands in between the controller layer and repository layer.

Design service layer

First, let’s create a folder called services. Inside the pkg folder, let’s create a folder called services. And then create a folder inside services folder, called dtos. Then we will create a phoneNumber.go file inside the dtos folder.

package dtos

type ValidatePhoneNumberRequest struct {

	PhoneNumber string `json:"phone_number"`
	
}

Dtos stands for Data Transfer Object. It is used for transfer the data between the controller and service layer. In our project, we will use the dto to handle the phone number from API from user and transfer to service to do more calculation.

Next, we will create a file called phoneNumber.go inside the services folder. And start design the interface for our service.

package serivces

import (
	"phone_validator/pkg/repositories"
	"phone_validator/pkg/serivces/dtos"
)

type PhoneValidatorService interface {
	ValidatePhoneNumber(req dtos.ValidatePhoneNumberRequest) (bool, error)
}
  
type phoneValidatorService struct {
	storage repositories.PhoneNumberRepository
}

Our PhoneValidatorService will expose a function for controller which will handle the validation and save the result.

We will use the phone numbers library to validate our phone number. So let’s install it first.

go get -u github.com/nyaruka/phonenumbers

Then, In the same phoneNumber.go file, we will started to implement the service.

package serivces

import (
	"phone_validator/pkg/repositories"
	"phone_validator/pkg/repositories/entities"
	"phone_validator/pkg/serivces/dtos"
	
	"github.com/nyaruka/phonenumbers"
)


type PhoneValidatorService interface {
	// The interface 
}

type phoneValidatorService struct {
	// The struct
}

// Step 2: create the constructor for the service
func NewPhoneValidatorService(repo repositories.PhoneNumberRepository) PhoneValidatorService {
	return &phoneValidatorService{	
		storage: repo,
	}
}

// Step 3: implement the methods for the service
func (p *phoneValidatorService) ValidatePhoneNumber(req dtos.ValidatePhoneNumberRequest) (bool, error) {
	phoneNumberToValidate := req.PhoneNumber
	// 1. Check if phone number is empty or spaces
	if len(phoneNumberToValidate) == 0 || phoneNumberToValidate == " " {
		return false, nil
	}

	// 2. Check if phone number is existing in the database
	phoneRecord, err := p.storage.GetPhoneNumberResult(phoneNumberToValidate)

	if err != nil {
		return false, err
	}

	// 3. Return true if phone number is existing in the database
	if phoneRecord.PhoneNumber != "" {
		return phoneRecord.Valid, nil
	}

	// I'm in Taiwan so I'll use TW to validate phone number in Taiwan, feel free to change to any other code
	// 4. parse the phone number	
	phoneNumber, err := phonenumbers.Parse(phoneNumberToValidate, "TW")

	if err != nil {
		return false, err
	}

	isValid := phonenumbers.IsValidNumberForRegion(phoneNumber, "TW")

	// 5. Save the phone number to the database
	phoneNumberToSave := entities.PhoneNumber{
		PhoneNumber: phoneNumberToValidate,
		Valid: isValid,
	}
	
	res, err := p.storage.CreatePhoneNumberResult(phoneNumberToSave)

	if err != nil {
		return false, err
	}

	return res.Valid, nil
}

Implement the Controller layer

We had implement the repository layer for retrieve and convert the data from DB and service layer for the API functionality. Now, let’s create the outermost layer for our API --- the controller. Our controller will be responsible for parsing the data from the HTTP request body, and sent back the validation result.

Inside the pkg folder, let’s create a folder called controllers. Then we will create a phoneNumber.go file inside the controllers folder. And we will start to write the logic for our controller.

Our controller will expose one API endpoint the client /api/validate, this endpoint will receive a JSON body and parse the data then handover the data to our service.

package controllers

import (
	"log"
	"net/http"
	"phone_validator/pkg/serivces"
	"phone_validator/pkg/serivces/dtos"
	"github.com/gin-gonic/gin"
)

  

// Step 1: define the struct for the controller
type PhoneValidatorHandler struct {

PhoneValidatorService serivces.PhoneValidatorService

}

  

// Step 2: create the constructor for the controller
func NewPhoneValidatorHandler(e *gin.Engine, phoneValidatorService serivces.PhoneValidatorService) {
	handler := &PhoneValidatorHandler{
	
	PhoneValidatorService: phoneValidatorService,
	
	}

    // Register the endpoint
	e.POST("/api/validate", handler.ValidatePhoneNumber)
}

// Step 3: implement the methods for the controller
func (handler *PhoneValidatorHandler) ValidatePhoneNumber(c *gin.Context) {
	
	c.Header("Content-Type", "application/json")
	var phoneToValidate dtos.ValidatePhoneNumberRequest
	
	// 4. Get the request body
	err := c.ShouldBindJSON(&phoneToValidate)
	
	if err != nil {
		log.Printf("handler error: %v", err)
		c.JSON(http.StatusBadRequest, nil)
		return
	}
	
	// 5. Call the service
	isValid, err := handler.PhoneValidatorService.ValidatePhoneNumber(phoneToValidate)
	
	if err != nil {
		log.Printf("handler error: %v", err)
		c.JSON(http.StatusInternalServerError, gin.H{
		"error": err.Error(),
		})

		return
	}
	
	// 6. Return the response
	c.JSON(http.StatusOK, gin.H{
		"error": nil,
		"valid": isValid,
	})
}

Bring it all together

Great! So far we have create all the things we need for our API. Let’s combine them together to make sure the API works as expected!

Back to our main.go file. We will instantiate the the repository, service and controller inside the run() function.

package main

import (
	"fmt"
	"os"
	
	"phone_validator/pkg/controllers"
	models "phone_validator/pkg/db"
	"phone_validator/pkg/repositories"
	"phone_validator/pkg/serivces"

	"github.com/gin-contrib/cors"
	"github.com/gin-gonic/gin"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

// Entry point
func main() {/**/}

func run() error {
	dsn := "root:root@tcp(127.0.0.1:3306)/phone_validator?charset=utf8mb4&parseTime=True&loc=Local"
	
	db, err := setUpDbConnection(dsn)

	// This will create the phone number table in the database
	db.AutoMigrate(&models.PhoneNumber{})
	
	if err != nil {
		return err
	}

	// Instantiate the repository and inject the db instance
	phoneRepo := repositories.NewPhoneNumberResultRepository(db)

	// Instantiate the service and inject the repository instance
	phoneValidatorService := serivces.NewPhoneValidatorService(phoneRepo)
	
	// Init gin router to handle API call
	router := gin.Default()

	// Set up cors
	router.Use(cors.Default())

	// Instantiate the controller and inject the service instance and gin router instance.
	controllers.CreatePhoneValidatorHandler(router, phoneValidatorService)

	// Start the server at port 3000
	err = router.Run(":3000")

	if err != nil {
		fmt.Printf("Server - there was an error calling Run on router: %v", err)
		return err
	}

	return nil
}

func setUpDbConnection(connectionString string) (*gorm.DB, error) {/**/}

Run the server and test the API

Now let’s try to run the server and test our API.

In terminal type the command below

go run main.go

And open Postman fill in http://localhost:3000/api/validate as the URL and choose POST . Remember the check the raw radio button in the body tab and select the JSON for the body type. Then fill in the text area below with the number you wish to validate. Then click send.

Postman UI

You should see the result like this since the number is not valid.

{
	"error": null,
	"valid": false
}

You can try another phone number and it should sent back the correct response according to your input. If not, please go to the service layer and see if you change the TW code to where your input phone number belong in below lines.

// I'm in Taiwan so I'll use TW to validate phone number in Taiwan, feel free to change to any other code
// 4. parse the phone number	
phoneNumber, err := phonenumbers.Parse(phoneNumberToValidate, "TW")

if err != nil {
	return false, err
}

isValid := phonenumbers.IsValidNumberForRegion(phoneNumber, "TW")

Summary

This is the last article for the Develop a phone validator with Go using dev container.

In this series we have learn how to:

  1. Write basic docker-compose file to integrate our server and database.
  2. Use gorm as an ORM to communicate with the database.
  3. The basic concept for the N-teir architecture, and simple Dependency injection with Go.
  4. How to write a scalable Go service.

If you want to go deeper, here are things you can try base on this project.

  1. Add OpenAPI documentation, For example: Swagger.
  2. Write unit test for this API service.
  3. validate the user input on controller.

📕 Previous Posts:

1️⃣ Develop an API with Go using dev container - 1

2️⃣ Develop an API with Go using dev container - 2