Develop an API with Go using dev container - 3
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:
- create a validation result with the
phoneNumber
- 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.
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:
- Write basic docker-compose file to integrate our server and database.
- Use gorm as an ORM to communicate with the database.
- The basic concept for the N-teir architecture, and simple Dependency injection with Go.
- How to write a scalable Go service.
If you want to go deeper, here are things you can try base on this project.
- Add OpenAPI documentation, For example: Swagger.
- Write unit test for this API service.
- validate the user input on controller.