64

Let’s Get It Started! - Learning Cloud Native Go - Medium

 4 years ago
source link: https://medium.com/learning-cloud-native-go/lets-get-it-started-dc4634ef03b
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Image for post
Image for post

Let’s Get It Started!

Building a Dockerized RESTful API application using Go - microservices

Cloud Native Application Development is a one way of speeding up building web applications, using micro-services, containers and orchestration tools. First, let’s see what is a cloud and what cloud native means.

What is Cloud Native?

There are many definitions for cloud and cloud native architecture. First let’s check the definitions given by RedHat via their Understanding cloud computing and Understanding cloud-native applications articles.

📖 Clouds are pools of virtual resources (such as raw processing power, storage, or cloud-based applications) orchestrated by management and automation software so they can be accessed by users on-demand through self-service portals supported by automatic scaling and dynamic resource allocation.

📖 Cloud native applications are a collection of small, independent and loosely coupled services which are specifically designed to provide a fast and consistent development and automated management experience across private, public, and hybrid clouds.

I think, you are now quite clear about cloud and cloud native applications. The definition provided by Cloud Native Computing Foundation, explains that how Cloud native applications achieve a fast and consistent development.

📖 Cloud native computing uses an open source software stack to deploy applications as microservices, packaging each part into its own container, and dynamically orchestrating those containers to optimize resource utilization. Cloud native technologies enable software developers to build great products faster.

Explaining the concepts behind cloud native architecture is beyond the purpose of this article. But I would like to highly recommend you to read the first chapter of Cloud Native DevOps with Kubernetes which is written by John Arundel and Justin Domingus. You can get this as a free e-book via NGINX resources as well.

The First Step

This is the very beginning of this project. So, as the first step, we are going to discuss about “How to build a Dockerized RESTful API application using Go”. In the future discussions, we will discuss about Kubernetes and how to deploy our application in a cloud environment.

In this post, we are going to build a RESTful API application to manage a simple bookshelf. And in here, we are discussing,
01. Creating a new Go project.
02. Adding initial HTTP server.
03. Adding initial Docker files.
04. Adding initial configurations.
05. Adding Chi router.
06. Adding Zerolog logger.
07. Adding DB docker file.
08. Adding initial database migrations.
09. Adding GORM.
10. Adding initial books API routes.
11. Implementing RESTful handlers.
12. Adding Validator.v9.

Contribute! 🥤 Buy me a coffee! 🚀 Hire me!

The completed project can be found in learning-cloud-native-go/myapp GitHub repository and you can check the code on each step by using step- branches of that repository. The completed API application supports the following API endpoints.

Image for post
Image for post

OK, Let’s get it started!

Creating a new Go project

Creating a remote repository

It’s a good practice to save the code in a remote repository. So, I am using https://github.com/learning-cloud-native-go/myapp to store the code.

💡 You can use GitHub, Bitbucket, GitLab or any preferable version control repository hosting service to store the code.

In here, I prefer to use myapp as the project name. But you can choose a better name according to the purpose of your application.

We are going to implement this application as a Go module. So, better use any folder outside the GOPATH to store the code locally. In here, I am using dev folder inside my home folder.

cd ~/dev
git clone [email protected]:learning-cloud-native-go/myapp.git

Creating a Go module

📖A Go module is a collection of related Go packages that are versioned together as a single unit. Most often, a version control repository contains exactly one module defined in the repository root. (Multiple modules are supported in a single repository, but typically that would result in more work on an on-going basis than a single module per repository).

Use go mod init command with the project name, inside the project folder to make it a Go module.

cd myapp
go mod init myapp

💡 If you want to reuse this module inside another project, better use go mod init with either github.com/mycompany/myapp or mycompany.com/myapp.

This creates the go.mod file with the following content,

💭 In the next steps, I assume you are inside the myapp folder while mentioning file paths and commands to run.

Adding initial HTTP server

💡 If you are a newcomer to Go, I recommend you to read the overview of the documentation of the net/httppackage, the descriptions of ServeMux type and HandleFunc & ListenAndServe functions, before continuing this.

It’s a convention to store executable packages inside cmd directory. So, let’s save this code under cmd/app/main.go.

▸ You can use go run cmd/app/main.gocommand, to run it locally.
▸ You should see Hello World! text while visit localhost:8080in the browser.

🔎 Search why we need to set custom timeouts on default http.Server.

Adding initial docker files

📖 Docker is a platform for developers and sysadmins to develop, deploy, and run applications with containers.

📖 A container is a standard unit of software that packages up code and all its dependencies so the application runs quickly and reliably from one computing environment to another.

💡 If you are a newcomer to Docker, I recommend you to read What is a Container? article and its Get Started guild on its official documentation.

Adding a Dockerfile

▸ I would like to store all Docker related files inside the docker folder in the project root, except the docker-compose.yml .
▸ We are going to add another Dockerfile for the database. So, let’s save this code under docker/app/Dockerfile .
▸ In here, we are using golang:1.13-alpine base image with gcc, musl-dev , git, bash packages.

💡 If you don’t like to add an extra weight to your images by installing bash, just use builtin ash or /bin/sh shells.

🔎 Search the difference between golang, golang:alpine , golang:stretch , alpine and stretch Docker images.

Adding a docker-compose.yml

📖 Compose is a tool for defining and running multi-container Docker applications. With a single command, we can create and start all the services according to the content in the docker-compose.yml file.

💡 If you are new to Docker Compose, I recommend you to read its Get Started guild on its official documentation.

As mentioned earlier, we keep the docker-compose.yml in the project root.

▸ You can use docker-compose buildand docker-compose up commands, to build and run the application.
▸ You should see Hello World! text while visit localhost:8080in the browser.

Support Docker multi-stage builds

Docker images with the Go SDK is quite large. Because of Go is a statically compiled language, after creating the executable files, we don’t need the Go SDK and the codebase to run the application.

golang      1.13-alpine      359MB
alpine latest 5.58MB

Docker multi-stage build is a new feature which is using a single Docker file with multiple stages to build the application in one environment and copy each executable and run on another environment to reduce the resources needed in the production environment.

Let’s update the docker/app/Dockerfile to support multi-stage builds.

▸ After updating the Dockerfile, we need to rebuild and rerun the application.
▸ Always run docker-compose down before running docker-compose up to stop running containers and remove containers, networks, volumes, and images created by previous up commands.

💡 The docker images command shows all top-level Docker images with their ID, repository name, tag name and the size. You can use the docker rmi -f command with the image ID to delete any old images from your system.
💡 Better skip using Docker multi-stage builds in the development environment, due to we can reduce the build time of the second step.

Adding initial configurations

If you remember the code of initial HTTP server at step-2, we have hard-coded the server port and timeout values with the code. Things like them should be configurable. So better extract them to a config package and use those config parameters in the code.

💭 To store configurations, we can use many formats like .xml, .json, .env, .yaml, .toml files or systems like etcd, AWS Parameter Store, GCP Runtime Configurator. I chose .env files to store configurations in here due to the simplicity of usage.

Using environment variables for configurations

▸ Because of these configs should be loaded with Docker application start, we store them under docker/app/.env .

▸ We can use env_file configuration option on docker-compose.yml to set environment variables via an .env file on application start; (Line 5-6).

Populating data from environment variables

💭 Go standard library provides os.Getenv() function to read each environment variable separately. But there are Go libraries like spf13/viper, kelseyhightower/envconfig, caarlos0/env, joeshaw/envdecode to get data on multiple environment variables as a set, by populating a struct from environment variables. I chose joeshaw/envdecode to use in here, due to its simplicity.

▸ We need to run go get github.com/joeshaw/envdecode to download and install the package. This will update go.mod and go.sum files as well.

▸ We can cache go modules by copying go.mod and go.sum files to Docker first and by running go mod download before copying all other files to Docker. So, let’s update docker/app/Dockerfile for this; (Line 6-8).

▸ Let’s create a config package to get all configuration values once by adding Conf struct to map each configuration and AppConfig() function to get the populated struct. I save this code under config/config.go .

💡 In here, we have created Conf struct with embedded struct serverConf to get initial server configs. So, when we need to add new configurations for DB in the future, we can easily add another embedded struct for them to maintain simplicity and readability of the code.

▸ Then, let’s update cmd/app/main.go to get configuration values via AppConfig() function; (Line 6,10,15 and 22–24).

▸ Rebuild and rerun the application. You should see the same Hello World! response on localhost:8080 .

Adding Chi router

💭 The default HTTP request multiplexer in net/http is not very powerful. For example, if you visit localhost:8080/invalid-path it gives the same Hello World! response with 200 HTTP status instead giving 404 HTTP status. There are many powerful router libraries in Go like gorilla/mux, go-chi/chi, julienschmidt/httprouter, buaazp/fasthttprouter. In here, we are using go-chi/chi due to its lesser weight and extensibility.

▸ We need to run go get github.com/go-chi/chi to download and install the package. As you know, this updates go.mod and go.sum files as well.

⭐ Instead of using cmd/app to store all application logic, it’s a good practice to create a separate package to store the main application code and call it from cmd/app while creating the executable. So, I am creating the app package in the project root for this. However, you can name this as server or http, according to the purpose of the application.

▸ Let’s create a simple Handler which gives the Hello World! text response. I save this under app/app/indexHandler.go . It’s a simple function which matches net/http handler signature func(ResponseWriter, *Request) to pass to func (*ServeMux) HandleFunc with a URL pattern.

💡 In the next steps, we are going to build a struct which contains main dependencies like DB connection, under app/app to use them with handlers. If you don’t like the name app/app , I recommend you to name this as app/server or server/app .

▸ Then, let’s create a new Chi router and bundle our handler to "/" route pattern via MethodFunc() . I save this under app/router/router.go .

💡 If you are not familiar with Chi router, I recommend you to read its README for more details. If you are new to Go, again I recommend you to read the overview of the documentation of the net/httppackage before that. As you see MethodFunc() is matching http.HandleFunc() signature.

▸ After that, we need to update cmd/app/main.go to use our Chi router instead using default HTTP route multiplexer; (Line 6, 13 and 21).

▸ Rebuild and rerun the application. You should see the same Hello World! response on localhost:8080. And this time, you should see 404 page not found response if you visit localhost:8080/invalid-path.

Adding zerolog logger

While using microservices architectures, multiple services might be invoked to handle a single client request. The Syslog is a message logging protocol which can be used to send all log events in different systems to a centralized log storage like Graylog, Stackdriver, ELK Stack, to provide visibility into the behavior of microservices. There are many powerful logging libraries which support Syslog standards in Go like uber-go/zap, apex/log, sirupsen/logrus, rs/zerolog. In here, we are using rs/zerolog due to its speed and lesser allocations.

Adding Zerolog as the Syslog logger

▸ We need to run go get github.com/rs/zerolog to download and install the package. As you know, this updates go.mod and go.sum files as well.

▸ Let’s create a global logger under util/logger/logger.go by wrapping zerolog.Logger. We can find an example under rs/zerolog/log package.

💡 Above code block shows how to convert the code in rs/zerolog/log to util/logger/logger.go. Due to we need to prevent using lengthy code blocks in the blog post, I have added only one method/ Output() in the above code. The complete code can be found on util/logger package with tests.

▸ In the previous step, we used isDebug boolean parameter to choose the log level. We set the log level as Debug, only if that value is true. Otherwise, we set it as Info. Let’s add this to our configurations by adding DEBUG=true to docker/app/.env and by adding Debug bool `env:"DEBUG,required"` to config/config.go Conf struct. Check the step-6 branch for the complete code.

▸ Let’s update cmd/app/main.go to use new util/logger instead the default log package.

Implementing a request logger

💭 Besides the main application events, it’s helpful having logs of each request and response details. For that we can create a custom Handler which maps ServeHTTP(ResponseWriter, *Request) signature and an embedded util/logger to log request and response details.
🔎 A good example can be found in google/go-cloud/server/requestlog.

▸ Let’s create the new custom Handler under app/handler/handler.go. Later, we will use the NewHandler() function to convert normal handlers to this custom handlers. If you don’t like the naming app/handler use app/requestlog.

▸ To keep the code cleaner, the code related with log entry is moved to app/handler/logEntry.go.

Creating the main app package

💭 In the previous steps, we just created a global Syslog logger and a custom Handler which logs each request and response details. We need to integrate both of them with our application handles like app/app/indexHandler.go.

▸ One way to set the logger as a dependency for our server application is, creating a struct with embedded rs/zerolog logger, to represent our server application and then attach our application handles to that struct. For that, let’s create the App struct under app/app/app.go. If you don’t like the naming app/app/app.go , I recommend you to name this as app/server/server.go with Server struct or server/app/app.go with App struct.

However, this is not the most efficient way of packaging Go applications. But to make the project more accessible for different levels of programmers, especially for the newcomers to Go web API development, we assume all API always depends on the same set of dependencies.

💡With this structure, we can easily load application dependencies inside handlers. For the moment it’s only about a Syslog logger. Same way, we can add the DB connections, Redis connections, messaging systems like NATS and etc.

▸ Then, let’s convert the HandleIndex() function in app/app/indexHandler.go to a method of above App struct.
func (app *App) HandleIndex(w http.ResponseWriter, _ *http.Request) {

▸ Now, let’s update app/router/router.go to use NewHandler() function in app/handler to convert the normal handlers into the custom handlers which logs request and response details.

▸ Due to we are passing *App to the New() function in app/router , we have to update cmd/app/main.go.

▸ Rebuild and rerun the application. You should see the same Hello World! response on localhost:8080, as well as logs like this.

{"level":"info","received_time":"2019-08-05T13:37:29Z","method":"GET","url":"/","header_size":322,"body_size":0,"agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0","referer":"","proto":"HTTP/1.1","remote_ip":"172.18.0.1","server_ip":"172.18.0.2","status":200,"resp_header_size":96,"resp_body_size":12,"latency":0.068137,"time":"2019-09-02T13:37:29Z"}

Adding DB docker file

▸ As I informed earlier, we use the docker folder in the project root to store all Docker related files. So, Let’s save this under docker/mariadb/Dockerfile.

💡 In here, I am just using a mariadb alpine image with bash. If you don’t like to add an extra weight to your images by installing bash, just use builtin sh or ash shells and directly add image: "yobasystems/alpine-mariadb:latest" to docker-compose.yml without using build: in the next step.

▸ Then, let’s update docker-compose.yml;(From line 9)

▸ Rebuild and rerun the application. Now you should see two containers are running while you run docker-compose ps (or docker ps)

💡 Also you can use any mariadb client to connect to the (empty) database.

Image for post
Image for post

Adding initial database migrations

💭 Database migrations are like version controls for the database. There are few popular options for database migration in the Go ecosystem like golang-migrate/migrate, pressly/goose, GORM migrations and etc. In here, we are using pressly/goose, due to its lesser resource usage and the simplicity of usage.

Implementing a DB adapter

▸ In the previous section, inside the docker-compose.yml file, we adjusted the configurations of the MariaDB instance by setting environment variables to overwrite the default database name, username and passwords. From the Go application side, we need those configurations to connect with the database. So, let’s add those to docker/app/.env.

▸ To read those from the Go application side, we need to add those to Conf struct in config/config.go file.

▸ Go standard library provides a generic interface around SQL databases via the database/sql package. But we have to use a specific database driver implementation according to the the database driver we use. In here we use go-sql-driver/mysql as the MariaDB driver. So we need to run go get github.com/go-sql-driver/mysql to download and install the package. As you know, this updates go.mod and go.sum files as well.

▸ Let’s create the database adapter in adapter/db/db.go.

Implementing a DB migration tool using Goose

▸ We need to run go get github.com/pressly/goose to download and install the package.

▸ Let’s create a custom pressly/goose binary. A good example can be found in https://github.com/pressly/goose/blob/master/cmd/goose/main.go. We save this under cmd/migrate/main.go with few modifications to remove few unnecessary codes, due to we are using only MariaDB in our application.

💡 If you are not familiar with pressly/goose, now it’s the time to read its README for more details. Also in here, we are going to use only .sql migrations. So, I set the migrations directory to /myapp/migrations.

▸ Due to we need to build this package and copy the binary file to the smaller alpine image with .sql migration files, we have to update docker/app/Dockerfile; (Line 1,2,11,12).

▸ Rebuild and rerun the application. Login to myapp_app_1 container by running docker exec -it myapp_app_1 bash and run /myapp/migrate status. You should see no migrations available.

Applied At                  Migration
=====================================

Adding initial SQL migrations

▸ As mentioned in the very beginning of this post, we are building a RESTful CRUD API for a bookshelf. So, let’s create our first sql migration file to create books table under migrations/20190805170000_create_books_table.sql.

💡 We can use create command of our goose binary; ex: /myapp/migrate create create_books_table sql.

▸ Rebuild and rerun the application. Login to myapp_app_1 container by running docker exec -it myapp_app_1 bash and run /myapp/migrate status. Now, you should see one pending migration is available.

Applied At        Migration
==========================================================
Pending -- 20190805170000_create_books_table.sql

💡 We can run migrations manually with /myapp/migrate up command. But, let’s see how to automate running migrations in the development environment.

Running migrations at the application startup

🔎 This step is quite tricky, due to we need to wait till myapp_bd_1 container starts, to run database migrations. Otherwise, myapp_app_1 will be stopped with an error, if migrations run before creating the database.

💡In here, I am going to install mysql-client package to the myapp_app docker image and wait till database is running, by checking the database connection via a MySQL command-line client call.

▸ Let’s save the script which checks the database connection, under docker/app/bin/wait-for-mysql.sh.

▸ We need to update the docker/app/Dockerfile file, to install mysql-client, copy above script to the deployment environment and make them executable; (Line 4, 10,11).

▸ To run migrations at the applications startup and delete mysql-client after running migrations, let’s use this under docker/app/bin/init.sh.

💡 Due to we already copy files in docker/app/bin to deployment environment, no need to update docker/app/Dockerfile.

▸ Let’s use above scripts inside docker-compose.yml to safely run migrations after database starts.

▸ Rebuild and rerun the application. You can see that database migrations are running while starting the application by reviewing the logs.

Adding GORM

💭 GORM is a full featured ORM for Golang. It supports database associations, preloading associated models, database transactions and many more. If you are not familiar with GORM, I highly recommend you to check its documentation before starting this section.

Implementing a GORM adapter

▸ We need to run go get github.com/jinzhu/gorm to download and install the package. As you know, this updates go.mod and go.sum files as well.

▸ Even though GORM uses go-sql-driver/mysql to connect with MySQL/ MariaDB databases, still it does not allow us to use existing *sql.DB connections. Instead, we need to use *gorm.DB. So, we need to create a new adapter for GORM under adapter/gorm/gorm.go.

Adding GORM adapter to the main app

▸ Let’s update the App struct to set the GORM connection as a dependency for our server application, by updating the app/app/app.go (Line 9,14,18).

▸ Then we need to update cmd/app/main.go to get the GORM connection at the startup of the application.

💡In here, we enable database logs according to the boolean value in Debug configurations.

Checking application health

▸ Adding handlers for liveness and readiness probes is a common practice in modern Go applications especially while using Kubernetes for deployments. With that, we can check the health of GORM connection as well. So, let’s add those under app/app/heathHandler.go.

▸ Then, we need to update app/router/router.go to attach those handlers to the router.

▸ Rebuild and rerun the application. You should get 200 HTTP status while visit localhost:8080/healthz/readiness in the browser if the GORM connection is healthy.

Adding initial books API routes

Implementing initial books API Handlers

▸ As mentioned earlier, in here we are building a RESTful CRUD API for a bookshelf. So, let’s create initial handler functions for the books API under app/app/bookHandler.go.

▸ We need to update app/router/router.go to add the handlers to the router.

▸ Rebuild and rerun the application. And then, test /books API by using a REST client application like Insomnia REST Client or Postman. You should see request logs like these on each request.

Implementing Content-Type JSON middleware

▸ Instead of setting HTTP header "Content-Type": "application/json" on each handler code, we can create a router middleware for this under app/router/middleware/content_type_json.go.

▸ Also, we can add tests under app/router/middleware/content_type_json_test.go to test the middleware.

▸ Then, update app/router/router.go to attach above middleware to API routes. In here, we prefix all API routes with /api/v1 as well.

▸ Rebuild and rerun the application. And now, you should see "Content-Type": "application/json" headers in API responses.

Implementing RESTful handlers

Let’s complete the functionality of each handler functions.

💭 To make examples simpler and easier to understand for different levels of programmers, in here I am using models and repositories. This may not be the most idiomatic way of structuring Go applications but, this is one of hassle-free structure, even an absolute newcomer can understand.

Completing list books functionality

▸ We use model/book.go to save models related to books. Book struct is used to map database records and BookDto struct is used to control how to show the model to outside.

GORM provides multiple ways to query data from the database. To get the all records from a batabase table, we can use its Find() method. So, let’s create the ListBooks() method in repository/book.go to get all books records.

▸ Now, let’s complete the HandleListBooks() method in app/app/bookHandler.go. If an error occurs inside the handler, we log the actual error and use a static message to create the error response.

▸ We save those static error messages inside app/app/app.go

Completing read book functionality

▸ To get a single record from the database, we can use GORM’s First() method. Let’s add ReadBook() method in repository/book.go to query a single book record.

💡 Because of we use the primary key to get data, we can use db.First(&book, id) directly, without using its Where() method.

▸ This is how we can complete HandleReadBook() method in app/app/bookHandler.go.

Completing delete book functionality

▸ To delete a database record, we can use GORM’s Delete() method. So, let’s add DeleteBook() method in repository/book.go to delete a single book record.

▸ Then, let’s complete the HandleDeleteBook() method in app/app/bookHandler.go.

Completing create book functionality

▸ To create a new database record, we can use GORM’s Create() method. So, let’s add CreateBook() method in repository/book.go to create a new book record.

▸ Let’s update model/book.go. Input structures may not match with the models or dtos. So in here, I am using a different structure for the form. But, if both input and output/ model structures are same, we can reuse same structures without creating new structures for forms.

▸ Then, let’s complete the HandleCreateBook() method in app/app/bookHandler.go.

💭 We will add form validations in the next section.

▸ We save those static error messages inside app/app/app.go.

Completing update book functionality

▸ To update the database records, we can use GORM’s Update() method. So, let’s add UpdateBook() method in repository/book.go to update a book record.

▸ Then, let’s complete the HandleUpdateBook() method in app/app/bookHandler.go.

💭 We will add form validations in the next section.

▸ We save those static error messages in app/app/app.go.

const appErrDataUpdateFailure = “data update failure”

▸ Rebuild and rerun the application. Now, the functionalities of book handlers should be worked, expect form validations which we are going to add in the next section.

Adding Validator.v9

💭 Form validation is an important step while inserting and updating data. In the Go ecosystem we can see few validation packages like go-playground/validator.v9 , go-ozzo/ozzo-validation. In here, we are using playground/validator.v9 due to its simplicity of usage.

Adding initial validator

▸ We need to run go get gopkg.in/go-playground/validator.v9 to download and install the package. As you know, this updates go.mod and go.sum files as well.

▸ Let’s create util/validator/validator.go to get the *validator.Validate with a custom configuration. By default, it uses validate struct field tags to read meta data. But, in here, we replace it with form struct field tags.

▸ To use this *validator.Validate as the global validator, let’s add this to App struct in app/app/app.go.

▸ Then, we need to update cmd/app/main.go to get this *validator.Validate at the startup of the application.

▸ Let’s add validation rules to the BookForm struct in the model/book.go.

💡 You can see all list of validation rule types, supported by playground/validator.v9 in https://github.com/go-playground/validator/blob/v9/baked_in.go#L64.

▸ Then, let’s add validations to the HandleCreateBook() and HandleUpdateBook() methods in the app/app/bookHandler.go.

▸ Rebuild and rerun the application. You should see error messages like following messages, while inserting invalid data.

{
"error": "Key: 'BookForm.Title' Error:Field validation for 'Title' failed on the 'required' tag"
}

💭 As you can see, those are not having valid JSON formats. And also messages are not suitable to show to the end user. So, we will add custom messages for these in the next section.

Implementing custom validation messages

We need to fix two things in the default error messages of playground/validator.v9.

01. It shows struct field names, instead of names in json tags; (“Field validation for ‘Title’ failed” instead “Field validation for ‘title’ failed”).

🔎 We can see the solution in https://github.com/go-playground/validator/blob/173026262523a492668bd6d78b8934c2ad69843f/validator_instance.go#L122 .

02. The error response is not having a valid JSON format and error messages are not suitable to show to the end users.

🔎 The solution playground/validator.v9 developers suggest is to use its go-playground/validator/translations package. But due to we don’t need to support multiple translations in our API application, we will write ToErrResponse() function in util/validator/validator.go to convert the default error messages to a valid JSON format with end user friendly error messages. However better check their implementation in https://github.com/go-playground/validator/blob/v9/translations/en/en.go.

▸ Let’s update util/validator/validator.go to fix both above issues.

▸ Then let’s update app/app/bookHandler.go.

⭐ As you can see, we duplicate 20 lines of code in each handler and it is not a good practice. 👨‍🏫 Assume this as the homework and find how we can remove these duplicates. 🔎 One way is, moving these codes to a private method in app/app/common.go.

▸ We save those static error messages in app/app/app.go.

const appErrFormErrResponseFailure = "form error response failure"

▸ Rebuild and rerun the application. Now, you should see error messages like these.

{
"errors": [
"title is a required field",
"author must be a maximum of 255 in length",
"image_url must be a valid URL"
]
}

Implementing custom validation types

▸ One last thing! We don’t fully validate the author’s name and the published date of the BookForm struct. For the moment playground/validator.v9 is not supporting “alphabetic characters with space” and “date” validations. So, let’s create a custom validation types for these in util/validator/validator.go.

▸ Then, we need to update BookForm struct in model/book.go.

▸ Rebuild and rerun the application. Now, you should get validation errors even for author and image_url.

{
"errors": [
"title is a required field",
"author can only contain alphabetic and space characters",
"published_date must be a valid date",
"image_url must be a valid URL"
]
}

Okay, Let’s stop our first post of Learning Cloud Native Go series in here. In this post, we discussed,

▸ Creating a new Go project ▸ Adding initial HTTP server ▸ Adding initial Docker files ▸ Adding initial configurations ▸ Adding Chi router ▸ Adding Zerolog Syslog logger ▸ Adding DB docker file ▸ Adding initial database migrations ▸ Adding GORM ▸ Adding initial books API routes ▸ Implementing RESTful handlers ▸ Adding Validator.v9

As mentioned at the beginning of this post, the completed project can be found in learning-cloud-native-go/myapp GitHub repository and you can check the code on each step by using step- branches in that repository.

Last but not least,
I’ll upload the same content to learning-cloud-native-go.github.io 👈 which uses Docusaurus in the upcoming days. Hope it will be more structured and easy to see the big picture.

Also, I am not a native English speaker. So, if you found any mistake or something I need to be changed, even a spelling or a grammar mistake, please let me know. Thanks for reading, hope it helped for you.

Contribute! 🥤 Buy me a coffee! 🚀 Hire me!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK