📖 Build a RESTful API on Go: Fiber, PostgreSQL, JWT and Swagger docs in isolated Docker containers
Tags: go, tutorial, beginners, postgres
Published: 2021-03-22 13:52:35

## Introduction

Hello, friends! 😉 Welcome to a **really** great tutorial. I've tried to make for you as simple step-by-step instructions as possible, based on a real-life application, so that you can apply this knowledge _here and now_.

I intentionally don't want to divide this tutorial into several disjointed parts, so that you don't lose the thought and focus. After all, I'm writing this tutorial **only to share my experience** and to show that backend development in Golang using the Fiber framework is easy!

At the end of the tutorial you will find a self-check block of knowledge, as well as a plan for further development of the application. So, I suggest you save the link to this tutorial to your bookmarks and share it on your social networks.

❤️ Like, 🦄 Unicorn, 🔖 Bookmark and let's go!

<a name="toc"></a>
### 📝 Table of contents

- [What do we want to build?](#what-do-we-want-to-build)
  - [API methods](#api-methods)
  - [Full application code for advanced users](#full-application-code-for-advanced-users)
- [My approach to Go project architecture](#my-approach-to-go-project-architecture)
  - [Folder with business logic only](#folder-with-business-logic-only)
  - [Folder with API Documentation](#folder-with-api-documentation)
  - [Folder with project-specific functionality](#folder-with-project-specific-functionality)
  - [Folder with platform-level logic](#folder-with-platform-level-logic)
- [Project configuration](#project-configuration)
  - [Makefile](#makefile)
  - [Fiber config in ENV file](#fiber-config-in-env-file)
  - [Docker network](#docker-network)
  - [PostgreSQL and initial migration](#postgresql-and-initial-migration)
  - [Dockerfile for the Fiber app](#dockerfile-for-the-fiber-app)
  - [Swagger](#swagger)
- [Practical part](#practical-part)
  - [Create a model](#create-a-model)
  - [Create validators for a model fields](#create-validators-for-a-model-fields)
  - [Create queries and controllers](#create-queries-and-controllers)
  - [The main function](#the-main-function)
  - [A middleware functions](#a-middleware-functions)
  - [Routes for the API endpoints](#routes-for-the-api-endpoints)
  - [Database connection](#database-connection)
  - [Useful utilities](#useful-utilities)
- [Testing the application](#testing-the-application)
- [Run project locally](#run-project-locally)
- [A self-check block of knowledge](#a-self-check-block-of-knowledge)
- [Plan for further development](#plan-for-further-development)

<a name="#what-do-we-want-to-build"></a>
## What do we want to build?

Let's create a REST API for an online library application in which we create new books, view them, and update & delete their information. But some methods will require us to authorize through providing a valid JWT access token. I'll store all the information about the books, as usual, in my beloved PostgreSQL.

I think, this functionality is enough to help you understand, how **easy** it is to work with [Fiber](https://github.com/gofiber/fiber) web framework to create a REST API in Go.

[↑ Table of contents](#toc)

<a name="#api-methods"></a>
### API methods

Public:

- **GET**: `/api/v1/books`, get all books;
- **GET**: `/api/v1/book/{id}`, get book by given ID;
- **GET**: `/api/v1/token/new`, create a new access token (_for a demo_);

Private (JWT protected):

- **POST**: `/api/v1/book`, create a new book;
- **PATCH**: `/api/v1/book`, update an existing book;
- **DELETE**: `/api/v1/book`, delete an existing book;

[↑ Table of contents](#toc)

<a name="#full-application-code-for-advanced-users"></a>
### Full application code for advanced users

If you feel strong enough to figure out the code yourself, the entire draft of this application is published in my GitHub repository:

{% github koddr/tutorial-go-fiber-rest-api %}

[↑ Table of contents](#toc)

<a name="#my-approach-to-go-project-architecture"></a>
## My approach to Go project architecture

Over the past two years, I have tried many structures for the Go application, but settled on mine, which I'll try to explain to you now.

[↑ Table of contents](#toc)

<a name="#folder-with-business-logic-only"></a>
### Folder with business logic only

`./app` folder doesn't care about _what database driver you're using_ or _which caching solution your choose_ or any third-party things.

- `./app/controllers` folder for functional controllers (used in routes);
- `./app/models` folder for describe business models and methods;
- `./app/queries` folder for describe queries for models;

[↑ Table of contents](#toc)

<a name="#folder-with-api-documentation"></a>
### Folder with API Documentation

`./docs` folder contains config files for auto-generated API Docs by Swagger.

[↑ Table of contents](#toc)

<a name="#folder-with-project-specific-functionality"></a>
### Folder with project-specific functionality

`./pkg` folder contains all the project-specific code tailored only for your business use case, like _configs_, _middleware_, _routes_ or _utilities_.

- `./pkg/configs` folder for configuration functions;
- `./pkg/middleware` folder for add middleware;
- `./pkg/routes` folder for describe routes of your project;
- `./pkg/utils` folder with utility functions (server starter, generators, etc);

[↑ Table of contents](#toc)

<a name="#folder-with-platform-level-logic"></a>
### Folder with platform-level logic

`./platform` folder contains all the platform-level logic that will build up the actual project, like _setting up the database_ or _cache server instance_ and _storing migrations_.

- `./platform/database` folder with database setup functions;
- `./platform/migrations` folder with migration files;

[↑ Table of contents](#toc)

<a name="#project-configuration"></a>
## Project configuration

The config of the project may seem very complicated at first sight. **Don't worry**, I'll describe each point as simply and easily as possible.

[↑ Table of contents](#toc)

<a name="#makefile"></a>
### Makefile

I highly recommend using a `Makefile` for faster project management! But in this article, I want to show the whole process. So, I will write all commands directly, without magic `make`.

> 👋 If you already know it, [here](https://github.com/koddr/tutorial-go-fiber-rest-api/blob/main/Makefile) is a link to the full project's `Makefile`.

[↑ Table of contents](#toc)

<a name="#fiber-config-in-env-file"></a>
### Fiber config in ENV file

I know that some people like to use YML files to configure their Go applications, but I'm used to working with classical `.env` configurations and don't see much benefit from YML (_even though I wrote an [article](https://dev.to/koddr/let-s-write-config-for-your-golang-web-app-on-right-way-yaml-5ggp) about this kind of app configuration in Go in the past_).

The config file for this project will be as follows:

```ini
# ./.env

# Server settings:
SERVER_URL="0.0.0.0:5000"
SERVER_READ_TIMEOUT=60

# JWT settings:
JWT_SECRET_KEY="secret"
JWT_SECRET_KEY_EXPIRE_MINUTES_COUNT=15

# Database settings:
DB_SERVER_URL="host=localhost port=5432 user=postgres password=password dbname=postgres sslmode=disable"
DB_MAX_CONNECTIONS=100
DB_MAX_IDLE_CONNECTIONS=10
DB_MAX_LIFETIME_CONNECTIONS=2
```

[↑ Table of contents](#toc)

<a name="#docker-network"></a>
### Docker network

Install and run [Docker](https://www.docker.com/get-started) service for your OS. By the way, in this tutorial I'm using the latest version (_at this moment_) `v20.10.2`.

OK, let's make a new Docker network, called `dev-network`:

```bash
docker network create -d bridge dev-network
```

We will use it in the future when we run the database and the Fiber instance in isolated containers. If this is not done, the two containers will not be able to communicate with each other.

> ☝️ For more information, please visit: https://docs.docker.com/network/

[↑ Table of contents](#toc)

<a name="#postgresql-and-initial-migration"></a>
### PostgreSQL and initial migration

So, let's start the container with the database:

```bash
docker run --rm -d \
	--name dev-postgres \
	--network dev-network \
	-e POSTGRES_USER=postgres \
	-e POSTGRES_PASSWORD=password \
	-e POSTGRES_DB=postgres \
	-v ${HOME}/dev-postgres/data/:/var/lib/postgresql/data \
	-p 5432:5432 \
	postgres
```

Check, if the container is running. For example, by [`ctop`](https://github.com/bcicen/ctop) console utility:

![ctop](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fq5qjopj8bb7q83bxmy2.png)

Great! Now we are ready to do the migration of the original structure. Here is the file for `up` migration, called `000001_create_init_tables.up.sql`:

```sql
-- ./platform/migrations/000001_create_init_tables.up.sql

-- Add UUID extension
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

-- Set timezone
-- For more information, please visit:
-- https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
SET TIMEZONE="Europe/Moscow";

-- Create books table
CREATE TABLE books (
    id UUID DEFAULT uuid_generate_v4 () PRIMARY KEY,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW (),
    updated_at TIMESTAMP NULL,
    title VARCHAR (255) NOT NULL,
    author VARCHAR (255) NOT NULL,
    book_status INT NOT NULL,
    book_attrs JSONB NOT NULL
);

-- Add indexes
CREATE INDEX active_books ON books (title) WHERE book_status = 1;
```

> ☝️ For easily working with an additional book attributes, I use `JSONB` type for a `book_attrs` field. For more information, please visit PostgreSQL [docs](https://www.postgresql.org/docs/11/datatype-json.html).

And `000001_create_init_tables.down.sql` for `down` this migration:

```sql
-- ./platform/migrations/000001_create_init_tables.down.sql

-- Delete tables
DROP TABLE IF EXISTS books;
```

Okay! We can roll this migration.

> 👍 I recommend to use [golang-migrate/migrate](https://github.com/golang-migrate/migrate) tool for easily up & down your database migrations in one console command.

```bash
migrate \
	-path $(PWD)/platform/migrations \
	-database "postgres://postgres:password@localhost/postgres?sslmode=disable" \
	up
```

[↑ Table of contents](#toc)

<a name="#dockerfile-for-the-fiber-app"></a>
### Dockerfile for the Fiber app

Create a `Dockerfile` in the project root folder:

```dockerfile
# ./Dockerfile

FROM golang:1.16-alpine AS builder

# Move to working directory (/build).
WORKDIR /build

# Copy and download dependency using go mod.
COPY go.mod go.sum ./
RUN go mod download

# Copy the code into the container.
COPY . .

# Set necessary environment variables needed for our image 
# and build the API server.
ENV CGO_ENABLED=0 GOOS=linux GOARCH=amd64
RUN go build -ldflags="-s -w" -o apiserver .

FROM scratch

# Copy binary and config files from /build 
# to root folder of scratch container.
COPY --from=builder ["/build/apiserver", "/build/.env", "/"]

# Export necessary port.
EXPOSE 5000

# Command to run when starting the container.
ENTRYPOINT ["/apiserver"]
```

Yes, I'm using two-staged container build and Golang `1.16.x`. App will be build with `CGO_ENABLED=0` and `-ldflags="-s -w"` to [reduce](https://golang.org/cmd/link/) size of the finished binary. Otherwise, this is the most common `Dockerfile` for any Go project, that you can use anywhere.

Command to build the Fiber Docker image:

```bash
docker build -t fiber .
```

> ☝️ Don't forget to add `.dockerignore` file to the project's root folder with all files and folders, which should be ignored when creating a container. Here is an [example](https://github.com/koddr/tutorial-go-fiber-rest-api/blob/main/.dockerignore), what I'm using in this tutorial.

Command to create and start container from image:

```bash
docker run --rm -d \
	--name dev-fiber \
	--network dev-network \
	-p 5000:5000 \
	fiber
```

[↑ Table of contents](#toc)

<a name="#swagger"></a>
### Swagger

As you can guess from the title, we're not going to worry too much about documenting our API methods. Simply because there is a great tool like [Swagger](https://swagger.io) that will do all the work for us!

- [swaggo/swag](https://github.com/swaggo/swag) package for easily generate Swagger config in Go;
- [arsmn/fiber-swagger](https://github.com/arsmn/fiber-swagger) official Fiber's middleware;

[↑ Table of contents](#toc)

![Practical part](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pwzmlh4zc2il8pv3e0j1.jpg)

<a name="#practical-part"></a>
## Practical part

Well, we have prepared all the necessary configuration files and the working environment, and we know what we are going to create. Now it's time to open our favorite IDE and start writing code.

> 👋 Be aware, because I will be explaining some points directly in the comments in the code, not in the article.

[↑ Table of contents](#toc)

<a name="#create-a-model"></a>
### Create a model

Before implementing a model, I always create a migration file with an SQL structure (_from the Chapter 3_). This makes it much easier to present all the necessary model fields at once.

```go
// ./app/models/book_model.go

package models

import (
	"database/sql/driver"
	"encoding/json"
	"errors"
	"time"

	"github.com/google/uuid"
)

// Book struct to describe book object.
type Book struct {
	ID         uuid.UUID `db:"id" json:"id" validate:"required,uuid"`
	CreatedAt  time.Time `db:"created_at" json:"created_at"`
	UpdatedAt  time.Time `db:"updated_at" json:"updated_at"`
	UserID     uuid.UUID `db:"user_id" json:"user_id" validate:"required,uuid"`
	Title      string    `db:"title" json:"title" validate:"required,lte=255"`
	Author     string    `db:"author" json:"author" validate:"required,lte=255"`
	BookStatus int       `db:"book_status" json:"book_status" validate:"required,len=1"`
	BookAttrs  BookAttrs `db:"book_attrs" json:"book_attrs" validate:"required,dive"`
}

// BookAttrs struct to describe book attributes.
type BookAttrs struct {
	Picture     string `json:"picture"`
	Description string `json:"description"`
	Rating      int    `json:"rating" validate:"min=1,max=10"`
}

// ...
```

> 👍 I recommend to use the [google/uuid](https://github.com/google/uuid) package to create unique IDs, because this is a more versatile way to protect your application against common number brute force attacks. _Especially if your REST API will have public methods without authorization and request limit_.

But that's not all. You need to write two special methods:

1. `Value()`, for return a JSON-encoded representation of the struct;
2. `Scan()`, for decode a JSON-encoded value into the struct fields;

They might look like this:

```go
// ...

// Value make the BookAttrs struct implement the driver.Valuer interface.
// This method simply returns the JSON-encoded representation of the struct.
func (b BookAttrs) Value() (driver.Value, error) {
	return json.Marshal(b)
}

// Scan make the BookAttrs struct implement the sql.Scanner interface.
// This method simply decodes a JSON-encoded value into the struct fields.
func (b *BookAttrs) Scan(value interface{}) error {
	j, ok := value.([]byte)
	if !ok {
		return errors.New("type assertion to []byte failed")
	}

	return json.Unmarshal(j, &b)
}
```

[↑ Table of contents](#toc)

<a name="#create-validators-for-a-model-fields"></a>
### Create validators for a model fields

Okay, let's define the fields we need to check on the input before passing them to the controller business logic:

- `ID` field, for checking a valid [UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier);

These fields are the biggest concern, because in some scenarios they will come to us from users. By the way, that's why we not only validate them, but consider them `required`.

And this is how I implement the validator:

```go
// ./app/utils/validator.go

package utils

import (
	"github.com/go-playground/validator/v10"
	"github.com/google/uuid"
)

// NewValidator func for create a new validator for model fields.
func NewValidator() *validator.Validate {
	// Create a new validator for a Book model.
	validate := validator.New()

	// Custom validation for uuid.UUID fields.
	_ = validate.RegisterValidation("uuid", func(fl validator.FieldLevel) bool {
		field := fl.Field().String()
		if _, err := uuid.Parse(field); err != nil {
			return true
		}
		return false
	})

	return validate
}

// ValidatorErrors func for show validation errors for each invalid fields.
func ValidatorErrors(err error) map[string]string {
	// Define fields map.
	fields := map[string]string{}

	// Make error message for each invalid field.
	for _, err := range err.(validator.ValidationErrors) {
		fields[err.Field()] = err.Error()
	}

	return fields
}
```

> 👌 I use [go-playground/validator](https://github.com/go-playground/validator) `v10` for release this feature.

[↑ Table of contents](#toc)

<a name="#create-queries-and-controllers"></a>
### Create queries and controllers

<a name=""></a>
#### Database queries

So as not to lose performance, I like to work with **pure** SQL queries without _sugar_, like `gorm` or similar packages. It gives a much better understanding of **how the application works**, which will help in the future not to make silly mistakes, when optimizing database queries!

```go
// ./app/queries/book_query.go

package queries

import (
	"github.com/google/uuid"
	"github.com/jmoiron/sqlx"
	"github.com/koddr/tutorial-go-fiber-rest-api/app/models"
)

// BookQueries struct for queries from Book model.
type BookQueries struct {
	*sqlx.DB
}

// GetBooks method for getting all books.
func (q *BookQueries) GetBooks() ([]models.Book, error) {
	// Define books variable.
	books := []models.Book{}

	// Define query string.
	query := `SELECT * FROM books`

	// Send query to database.
	err := q.Get(&books, query)
	if err != nil {
		// Return empty object and error.
		return books, err
	}

	// Return query result.
	return books, nil
}

// GetBook method for getting one book by given ID.
func (q *BookQueries) GetBook(id uuid.UUID) (models.Book, error) {
	// Define book variable.
	book := models.Book{}

	// Define query string.
	query := `SELECT * FROM books WHERE id = $1`

	// Send query to database.
	err := q.Get(&book, query, id)
	if err != nil {
		// Return empty object and error.
		return book, err
	}

	// Return query result.
	return book, nil
}

// CreateBook method for creating book by given Book object.
func (q *BookQueries) CreateBook(b *models.Book) error {
	// Define query string.
	query := `INSERT INTO books VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`

	// Send query to database.
	_, err := q.Exec(query, b.ID, b.CreatedAt, b.UpdatedAt, b.UserID, b.Title, b.Author, b.BookStatus, b.BookAttrs)
	if err != nil {
		// Return only error.
		return err
	}

	// This query returns nothing.
	return nil
}

// UpdateBook method for updating book by given Book object.
func (q *BookQueries) UpdateBook(id uuid.UUID, b *models.Book) error {
	// Define query string.
	query := `UPDATE books SET updated_at = $2, title = $3, author = $4, book_status = $5, book_attrs = $6 WHERE id = $1`

	// Send query to database.
	_, err := q.Exec(query, id, b.UpdatedAt, b.Title, b.Author, b.BookStatus, b.BookAttrs)
	if err != nil {
		// Return only error.
		return err
	}

	// This query returns nothing.
	return nil
}

// DeleteBook method for delete book by given ID.
func (q *BookQueries) DeleteBook(id uuid.UUID) error {
	// Define query string.
	query := `DELETE FROM books WHERE id = $1`

	// Send query to database.
	_, err := q.Exec(query, id)
	if err != nil {
		// Return only error.
		return err
	}

	// This query returns nothing.
	return nil
}
```

<a name=""></a>
#### Create model controllers

The principle of the `GET` methods:

- Make request to the API endpoint;
- Make a connection to the database (or an error);
- Make a query to get record(s) from the table `books` (or an error);
- Return the status `200` and JSON with a founded book(s);

```go
// ./app/controllers/book_controller.go

package controllers

import (
	"time"

	"github.com/gofiber/fiber/v2"
	"github.com/google/uuid"
	"github.com/koddr/tutorial-go-fiber-rest-api/app/models"
	"github.com/koddr/tutorial-go-fiber-rest-api/pkg/utils"
	"github.com/koddr/tutorial-go-fiber-rest-api/platform/database"
)

// GetBooks func gets all exists books.
// @Description Get all exists books.
// @Summary get all exists books
// @Tags Books
// @Accept json
// @Produce json
// @Success 200 {array} models.Book
// @Router /v1/books [get]
func GetBooks(c *fiber.Ctx) error {
	// Create database connection.
	db, err := database.OpenDBConnection()
	if err != nil {
		// Return status 500 and database connection error.
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
			"error": true,
			"msg":   err.Error(),
		})
	}

	// Get all books.
	books, err := db.GetBooks()
	if err != nil {
		// Return, if books not found.
		return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
			"error": true,
			"msg":   "books were not found",
			"count": 0,
			"books": nil,
		})
	}

	// Return status 200 OK.
	return c.JSON(fiber.Map{
		"error": false,
		"msg":   nil,
		"count": len(books),
		"books": books,
	})
}

// GetBook func gets book by given ID or 404 error.
// @Description Get book by given ID.
// @Summary get book by given ID
// @Tags Book
// @Accept json
// @Produce json
// @Param id path string true "Book ID"
// @Success 200 {object} models.Book
// @Router /v1/book/{id} [get]
func GetBook(c *fiber.Ctx) error {
	// Catch book ID from URL.
	id, err := uuid.Parse(c.Params("id"))
	if err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
			"error": true,
			"msg":   err.Error(),
		})
	}

	// Create database connection.
	db, err := database.OpenDBConnection()
	if err != nil {
		// Return status 500 and database connection error.
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
			"error": true,
			"msg":   err.Error(),
		})
	}

	// Get book by ID.
	book, err := db.GetBook(id)
	if err != nil {
		// Return, if book not found.
		return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
			"error": true,
			"msg":   "book with the given ID is not found",
			"book":  nil,
		})
	}

	// Return status 200 OK.
	return c.JSON(fiber.Map{
		"error": false,
		"msg":   nil,
		"book":  book,
	})
}

// ...
```

The principle of the `POST` methods:

- Make a request to the API endpoint;
- Check, if request `Header` has a valid JWT;
- Check, if expire date from JWT greather than now (or an error);
- Parse Body of request and bind fields to the Book struct (or an error);
- Make a connection to the database (or an error);
- Validate struct fields with a new content from Body (or an error);
- Make a query to create a new record in the table `books` (or an error);
- Return the status `200` and JSON with a new book;

```go
// ...

// CreateBook func for creates a new book.
// @Description Create a new book.
// @Summary create a new book
// @Tags Book
// @Accept json
// @Produce json
// @Param title body string true "Title"
// @Param author body string true "Author"
// @Param book_attrs body models.BookAttrs true "Book attributes"
// @Success 200 {object} models.Book
// @Security ApiKeyAuth
// @Router /v1/book [post]
func CreateBook(c *fiber.Ctx) error {
	// Get now time.
	now := time.Now().Unix()

	// Get claims from JWT.
	claims, err := utils.ExtractTokenMetadata(c)
	if err != nil {
		// Return status 500 and JWT parse error.
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
			"error": true,
			"msg":   err.Error(),
		})
	}

	// Set expiration time from JWT data of current book.
	expires := claims.Expires

	// Checking, if now time greather than expiration from JWT.
	if now > expires {
		// Return status 401 and unauthorized error message.
		return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
			"error": true,
			"msg":   "unauthorized, check expiration time of your token",
		})
	}

	// Create new Book struct
	book := &models.Book{}

	// Check, if received JSON data is valid.
	if err := c.BodyParser(book); err != nil {
		// Return status 400 and error message.
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
			"error": true,
			"msg":   err.Error(),
		})
	}

	// Create database connection.
	db, err := database.OpenDBConnection()
	if err != nil {
		// Return status 500 and database connection error.
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
			"error": true,
			"msg":   err.Error(),
		})
	}

	// Create a new validator for a Book model.
	validate := utils.NewValidator()

	// Set initialized default data for book:
	book.ID = uuid.New()
	book.CreatedAt = time.Now()
	book.BookStatus = 1 // 0 == draft, 1 == active

	// Validate book fields.
	if err := validate.Struct(book); err != nil {
		// Return, if some fields are not valid.
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
			"error": true,
			"msg":   utils.ValidatorErrors(err),
		})
	}

	// Delete book by given ID.
	if err := db.CreateBook(book); err != nil {
		// Return status 500 and error message.
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
			"error": true,
			"msg":   err.Error(),
		})
	}

	// Return status 200 OK.
	return c.JSON(fiber.Map{
		"error": false,
		"msg":   nil,
		"book":  book,
	})
}

// ...
```

The principle of the `PUT` methods:

- Make a request to the API endpoint;
- Check, if request `Header` has a valid JWT;
- Check, if expire date from JWT greather than now (or an error);
- Parse Body of request and bind fields to the Book struct (or an error);
- Make a connection to the database (or an error);
- Validate struct fields with a new content from Body (or an error);
- Check, if book with this ID is exists (or an error);
- Make a query to update this record in the table `books` (or an error);
- Return the status `201` without content;

```go
// ...

// UpdateBook func for updates book by given ID.
// @Description Update book.
// @Summary update book
// @Tags Book
// @Accept json
// @Produce json
// @Param id body string true "Book ID"
// @Param title body string true "Title"
// @Param author body string true "Author"
// @Param book_status body integer true "Book status"
// @Param book_attrs body models.BookAttrs true "Book attributes"
// @Success 201 {string} status "ok"
// @Security ApiKeyAuth
// @Router /v1/book [put]
func UpdateBook(c *fiber.Ctx) error {
	// Get now time.
	now := time.Now().Unix()

	// Get claims from JWT.
	claims, err := utils.ExtractTokenMetadata(c)
	if err != nil {
		// Return status 500 and JWT parse error.
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
			"error": true,
			"msg":   err.Error(),
		})
	}

	// Set expiration time from JWT data of current book.
	expires := claims.Expires

	// Checking, if now time greather than expiration from JWT.
	if now > expires {
		// Return status 401 and unauthorized error message.
		return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
			"error": true,
			"msg":   "unauthorized, check expiration time of your token",
		})
	}

	// Create new Book struct
	book := &models.Book{}

	// Check, if received JSON data is valid.
	if err := c.BodyParser(book); err != nil {
		// Return status 400 and error message.
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
			"error": true,
			"msg":   err.Error(),
		})
	}

	// Create database connection.
	db, err := database.OpenDBConnection()
	if err != nil {
		// Return status 500 and database connection error.
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
			"error": true,
			"msg":   err.Error(),
		})
	}

	// Checking, if book with given ID is exists.
	foundedBook, err := db.GetBook(book.ID)
	if err != nil {
		// Return status 404 and book not found error.
		return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
			"error": true,
			"msg":   "book with this ID not found",
		})
	}

	// Set initialized default data for book:
	book.UpdatedAt = time.Now()

	// Create a new validator for a Book model.
	validate := utils.NewValidator()

	// Validate book fields.
	if err := validate.Struct(book); err != nil {
		// Return, if some fields are not valid.
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
			"error": true,
			"msg":   utils.ValidatorErrors(err),
		})
	}

	// Update book by given ID.
	if err := db.UpdateBook(foundedBook.ID, book); err != nil {
		// Return status 500 and error message.
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
			"error": true,
			"msg":   err.Error(),
		})
	}

	// Return status 201.
	return c.SendStatus(fiber.StatusCreated)
}

// ...
```

The principle of the `DELETE` methods:

- Make a request to the API endpoint;
- Check, if request `Header` has a valid JWT;
- Check, if expire date from JWT greather than now (or an error);
- Parse Body of request and bind fields to the Book struct (or an error);
- Make a connection to the database (or an error);
- Validate struct fields with a new content from Body (or an error);
- Check, if book with this ID is exists (or an error);
- Make a query to delete this record from the table `books` (or an error);
- Return the status `204` without content;

```go
// ...

// DeleteBook func for deletes book by given ID.
// @Description Delete book by given ID.
// @Summary delete book by given ID
// @Tags Book
// @Accept json
// @Produce json
// @Param id body string true "Book ID"
// @Success 204 {string} status "ok"
// @Security ApiKeyAuth
// @Router /v1/book [delete]
func DeleteBook(c *fiber.Ctx) error {
	// Get now time.
	now := time.Now().Unix()

	// Get claims from JWT.
	claims, err := utils.ExtractTokenMetadata(c)
	if err != nil {
		// Return status 500 and JWT parse error.
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
			"error": true,
			"msg":   err.Error(),
		})
	}

	// Set expiration time from JWT data of current book.
	expires := claims.Expires

	// Checking, if now time greather than expiration from JWT.
	if now > expires {
		// Return status 401 and unauthorized error message.
		return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
			"error": true,
			"msg":   "unauthorized, check expiration time of your token",
		})
	}

	// Create new Book struct
	book := &models.Book{}

	// Check, if received JSON data is valid.
	if err := c.BodyParser(book); err != nil {
		// Return status 400 and error message.
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
			"error": true,
			"msg":   err.Error(),
		})
	}

	// Create a new validator for a Book model.
	validate := utils.NewValidator()

	// Validate only one book field ID.
	if err := validate.StructPartial(book, "id"); err != nil {
		// Return, if some fields are not valid.
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
			"error": true,
			"msg":   utils.ValidatorErrors(err),
		})
	}

	// Create database connection.
	db, err := database.OpenDBConnection()
	if err != nil {
		// Return status 500 and database connection error.
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
			"error": true,
			"msg":   err.Error(),
		})
	}

	// Checking, if book with given ID is exists.
	foundedBook, err := db.GetBook(book.ID)
	if err != nil {
		// Return status 404 and book not found error.
		return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
			"error": true,
			"msg":   "book with this ID not found",
		})
	}

	// Delete book by given ID.
	if err := db.DeleteBook(foundedBook.ID); err != nil {
		// Return status 500 and error message.
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
			"error": true,
			"msg":   err.Error(),
		})
	}

	// Return status 204 no content.
	return c.SendStatus(fiber.StatusNoContent)
}
```

<a name=""></a>
#### Method for get a new Access token (JWT)

- Make request to the API endpoint;
- Return the status `200` and JSON with a new access token;

```go
// ./app/controllers/token_controller.go

package controllers

import (
	"github.com/gofiber/fiber/v2"
	"github.com/koddr/tutorial-go-fiber-rest-api/pkg/utils"
)

// GetNewAccessToken method for create a new access token.
// @Description Create a new access token.
// @Summary create a new access token
// @Tags Token
// @Accept json
// @Produce json
// @Success 200 {string} status "ok"
// @Router /v1/token/new [get]
func GetNewAccessToken(c *fiber.Ctx) error {
	// Generate a new Access token.
	token, err := utils.GenerateNewAccessToken()
	if err != nil {
		// Return status 500 and token generation error.
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
			"error": true,
			"msg":   err.Error(),
		})
	}

	return c.JSON(fiber.Map{
		"error":        false,
		"msg":          nil,
		"access_token": token,
	})
}
```

[↑ Table of contents](#toc)

<a name="#the-main-function"></a>
### The main function

This is the most important feature in our entire application. It loads the configuration from the `.env` file, defines the Swagger settings, creates a new Fiber instance, connects the necessary groups of endpoints and starts the API server.

```go
// ./main.go

package main

import (
	"github.com/gofiber/fiber/v2"
	"github.com/koddr/tutorial-go-fiber-rest-api/pkg/configs"
	"github.com/koddr/tutorial-go-fiber-rest-api/pkg/middleware"
	"github.com/koddr/tutorial-go-fiber-rest-api/pkg/routes"
	"github.com/koddr/tutorial-go-fiber-rest-api/pkg/utils"

	_ "github.com/joho/godotenv/autoload"                // load .env file automatically
	_ "github.com/koddr/tutorial-go-fiber-rest-api/docs" // load API Docs files (Swagger)
)

// @title API
// @version 1.0
// @description This is an auto-generated API Docs.
// @termsOfService http://swagger.io/terms/
// @contact.name API Support
// @contact.email your@mail.com
// @license.name Apache 2.0
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
// @securityDefinitions.apikey ApiKeyAuth
// @in header
// @name Authorization
// @BasePath /api
func main() {
	// Define Fiber config.
	config := configs.FiberConfig()

	// Define a new Fiber app with config.
	app := fiber.New(config)

	// Middlewares.
	middleware.FiberMiddleware(app) // Register Fiber's middleware for app.

	// Routes.
	routes.SwaggerRoute(app)  // Register a route for API Docs (Swagger).
	routes.PublicRoutes(app)  // Register a public routes for app.
	routes.PrivateRoutes(app) // Register a private routes for app.
	routes.NotFoundRoute(app) // Register route for 404 Error.

	// Start server (with graceful shutdown).
	utils.StartServerWithGracefulShutdown(app)
}
```

[↑ Table of contents](#toc)

<a name="#a-middleware-functions"></a>
### A middleware functions

Since in this application I want to show how to use JWT to authorize some queries, we need to write additional middleware to validate it:

```go
// ./pkg/middleware/jwt_middleware.go

package middleware

import (
	"os"

	"github.com/gofiber/fiber/v2"

	jwtMiddleware "github.com/gofiber/jwt/v2"
)

// JWTProtected func for specify routes group with JWT authentication.
// See: https://github.com/gofiber/jwt
func JWTProtected() func(*fiber.Ctx) error {
	// Create config for JWT authentication middleware.
	config := jwtMiddleware.Config{
		SigningKey:   []byte(os.Getenv("JWT_SECRET_KEY")),
		ContextKey:   "jwt", // used in private routes
		ErrorHandler: jwtError,
	}

	return jwtMiddleware.New(config)
}

func jwtError(c *fiber.Ctx, err error) error {
	// Return status 401 and failed authentication error.
	if err.Error() == "Missing or malformed JWT" {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
			"error": true,
			"msg":   err.Error(),
		})
	}

	// Return status 401 and failed authentication error.
	return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
		"error": true,
		"msg":   err.Error(),
	})
}
```

[↑ Table of contents](#toc)

<a name="#routes-for-the-api-endpoints"></a>
### Routes for the API endpoints

- For public methods:

```go
// ./pkg/routes/private_routes.go

package routes

import (
	"github.com/gofiber/fiber/v2"
	"github.com/koddr/tutorial-go-fiber-rest-api/app/controllers"
)

// PublicRoutes func for describe group of public routes.
func PublicRoutes(a *fiber.App) {
	// Create routes group.
	route := a.Group("/api/v1")

	// Routes for GET method:
	route.Get("/books", controllers.GetBooks)              // get list of all books
	route.Get("/book/:id", controllers.GetBook)            // get one book by ID
	route.Get("/token/new", controllers.GetNewAccessToken) // create a new access tokens
}
```

- For private (JWT protected) methods:

```go
// ./pkg/routes/private_routes.go

package routes

import (
	"github.com/gofiber/fiber/v2"
	"github.com/koddr/tutorial-go-fiber-rest-api/app/controllers"
	"github.com/koddr/tutorial-go-fiber-rest-api/pkg/middleware"
)

// PrivateRoutes func for describe group of private routes.
func PrivateRoutes(a *fiber.App) {
	// Create routes group.
	route := a.Group("/api/v1")

	// Routes for POST method:
	route.Post("/book", middleware.JWTProtected(), controllers.CreateBook) // create a new book

	// Routes for PUT method:
	route.Put("/book", middleware.JWTProtected(), controllers.UpdateBook) // update one book by ID

	// Routes for DELETE method:
	route.Delete("/book", middleware.JWTProtected(), controllers.DeleteBook) // delete one book by ID
}
```

- For Swagger:

```go
// ./pkg/routes/swagger_route.go


package routes

import (
	"github.com/gofiber/fiber/v2"

	swagger "github.com/arsmn/fiber-swagger/v2"
)

// SwaggerRoute func for describe group of API Docs routes.
func SwaggerRoute(a *fiber.App) {
	// Create routes group.
	route := a.Group("/swagger")

	// Routes for GET method:
	route.Get("*", swagger.Handler) // get one user by ID
}
```

- `Not found` (404) route:

```go
// ./pkg/routes/not_found_route.go

package routes

import "github.com/gofiber/fiber/v2"

// NotFoundRoute func for describe 404 Error route.
func NotFoundRoute(a *fiber.App) {
	// Register new special route.
	a.Use(
		// Anonimus function.
		func(c *fiber.Ctx) error {
			// Return HTTP 404 status and JSON response.
			return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
				"error": true,
				"msg":   "sorry, endpoint is not found",
			})
		},
	)
}
```

[↑ Table of contents](#toc)

<a name="#database-connection"></a>
### Database connection

The database connection is the most important part of this application (_as well as any other, to be honest_). I like to break this process down into **two parts**.

- The method for the connection:

```go
// ./platform/database/open_db_connection.go

package database

import "github.com/koddr/tutorial-go-fiber-rest-api/app/queries"

// Queries struct for collect all app queries.
type Queries struct {
	*queries.BookQueries // load queries from Book model
}

// OpenDBConnection func for opening database connection.
func OpenDBConnection() (*Queries, error) {
	// Define a new PostgreSQL connection.
	db, err := PostgreSQLConnection()
	if err != nil {
		return nil, err
	}

	return &Queries{
		// Set queries from models:
		BookQueries: &queries.BookQueries{DB: db}, // from Book model
	}, nil
}
```

- The specific connection settings for the selected database:

```go
// ./platform/database/postgres.go

package database

import (
	"fmt"
	"os"
	"strconv"
	"time"

	"github.com/jmoiron/sqlx"

	_ "github.com/jackc/pgx/v4/stdlib" // load pgx driver for PostgreSQL
)

// PostgreSQLConnection func for connection to PostgreSQL database.
func PostgreSQLConnection() (*sqlx.DB, error) {
	// Define database connection settings.
	maxConn, _ := strconv.Atoi(os.Getenv("DB_MAX_CONNECTIONS"))
	maxIdleConn, _ := strconv.Atoi(os.Getenv("DB_MAX_IDLE_CONNECTIONS"))
	maxLifetimeConn, _ := strconv.Atoi(os.Getenv("DB_MAX_LIFETIME_CONNECTIONS"))

	// Define database connection for PostgreSQL.
	db, err := sqlx.Connect("pgx", os.Getenv("DB_SERVER_URL"))
	if err != nil {
		return nil, fmt.Errorf("error, not connected to database, %w", err)
	}

	// Set database connection settings.
	db.SetMaxOpenConns(maxConn)                           // the default is 0 (unlimited)
	db.SetMaxIdleConns(maxIdleConn)                       // defaultMaxIdleConns = 2
	db.SetConnMaxLifetime(time.Duration(maxLifetimeConn)) // 0, connections are reused forever

	// Try to ping database.
	if err := db.Ping(); err != nil {
		defer db.Close() // close database connection
		return nil, fmt.Errorf("error, not sent ping to database, %w", err)
	}

	return db, nil
}
```

> ☝️ This approach helps to connect additional databases more easily if required and always keep a clear hierarchy of data storage in the application.

[↑ Table of contents](#toc)

<a name="#useful-utilities"></a>
### Useful utilities

- For start API server (with a graceful shutdown or simple for dev):

```go
// ./pkg/utils/start_server.go

package utils

import (
	"log"
	"os"
	"os/signal"

	"github.com/gofiber/fiber/v2"
)

// StartServerWithGracefulShutdown function for starting server with a graceful shutdown.
func StartServerWithGracefulShutdown(a *fiber.App) {
	// Create channel for idle connections.
	idleConnsClosed := make(chan struct{})

	go func() {
		sigint := make(chan os.Signal, 1)
		signal.Notify(sigint, os.Interrupt) // Catch OS signals.
		<-sigint

		// Received an interrupt signal, shutdown.
		if err := a.Shutdown(); err != nil {
			// Error from closing listeners, or context timeout:
			log.Printf("Oops... Server is not shutting down! Reason: %v", err)
		}

		close(idleConnsClosed)
	}()

	// Run server.
	if err := a.Listen(os.Getenv("SERVER_URL")); err != nil {
		log.Printf("Oops... Server is not running! Reason: %v", err)
	}

	<-idleConnsClosed
}

// StartServer func for starting a simple server.
func StartServer(a *fiber.App) {
	// Run server.
	if err := a.Listen(os.Getenv("SERVER_URL")); err != nil {
		log.Printf("Oops... Server is not running! Reason: %v", err)
	}
}
```

- For generate a valid JWT:

```go
// ./pkg/utils/jwt_generator.go

package utils

import (
	"os"
	"strconv"
	"time"

	"github.com/golang-jwt/jwt"
)

// GenerateNewAccessToken func for generate a new Access token.
func GenerateNewAccessToken() (string, error) {
	// Set secret key from .env file.
	secret := os.Getenv("JWT_SECRET_KEY")

	// Set expires minutes count for secret key from .env file.
	minutesCount, _ := strconv.Atoi(os.Getenv("JWT_SECRET_KEY_EXPIRE_MINUTES_COUNT"))

	// Create a new claims.
	claims := jwt.MapClaims{}

	// Set public claims:
	claims["exp"] = time.Now().Add(time.Minute * time.Duration(minutesCount)).Unix()

	// Create a new JWT access token with claims.
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

	// Generate token.
	t, err := token.SignedString([]byte(secret))
	if err != nil {
		// Return error, it JWT token generation failed.
		return "", err
	}

	return t, nil
}
```

- For parse and validate JWT:

```go
// ./pkg/utils/jwt_parser.go

package utils

import (
	"os"
	"strings"

	"github.com/golang-jwt/jwt"
	"github.com/gofiber/fiber/v2"
)

// TokenMetadata struct to describe metadata in JWT.
type TokenMetadata struct {
	Expires int64
}

// ExtractTokenMetadata func to extract metadata from JWT.
func ExtractTokenMetadata(c *fiber.Ctx) (*TokenMetadata, error) {
	token, err := verifyToken(c)
	if err != nil {
		return nil, err
	}

	// Setting and checking token and credentials.
	claims, ok := token.Claims.(jwt.MapClaims)
	if ok && token.Valid {
		// Expires time.
		expires := int64(claims["exp"].(float64))

		return &TokenMetadata{
			Expires: expires,
		}, nil
	}

	return nil, err
}

func extractToken(c *fiber.Ctx) string {
	bearToken := c.Get("Authorization")

	// Normally Authorization HTTP header.
	onlyToken := strings.Split(bearToken, " ")
	if len(onlyToken) == 2 {
		return onlyToken[1]
	}

	return ""
}

func verifyToken(c *fiber.Ctx) (*jwt.Token, error) {
	tokenString := extractToken(c)

	token, err := jwt.Parse(tokenString, jwtKeyFunc)
	if err != nil {
		return nil, err
	}

	return token, nil
}

func jwtKeyFunc(token *jwt.Token) (interface{}, error) {
	return []byte(os.Getenv("JWT_SECRET_KEY")), nil
}
```

[↑ Table of contents](#toc)

![testing](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v6znat2q0yfoijsv555r.jpg)

<a name="#testing-the-application"></a>
## Testing the application

So, we're getting to the most important stage! Let's check our Fiber application through testing. I'll show you the principle by testing private routes (JWT protected).

> ☝️ As always, I will use Fiber's built-in [`Test()`](https://docs.gofiber.io/api/app#test) method and an awesome package [stretchr/testify](https://github.com/stretchr/testify) for testing Golang apps.

Also, I like to put the configuration for testing in a **separate file**, I don't want to mix a production config with a test config. So, I use the file called `.env.test`, which I will add to the root of the project.

Pay attention to the part of the code where routes are defined. We're calling the **real routes** of our application, so before running the test, you need to bring up the database (e.g. _in a Docker container for simplicity_).

```go
// ./pkg/routes/private_routes_test.go

package routes

import (
	"io"
	"net/http/httptest"
	"strings"
	"testing"

	"github.com/gofiber/fiber/v2"
	"github.com/joho/godotenv"
	"github.com/koddr/tutorial-go-fiber-rest-api/pkg/utils"
	"github.com/stretchr/testify/assert"
)

func TestPrivateRoutes(t *testing.T) {
	// Load .env.test file from the root folder.
	if err := godotenv.Load("../../.env.test"); err != nil {
		panic(err)
	}

	// Create a sample data string.
	dataString := `{"id": "00000000-0000-0000-0000-000000000000"}`

	// Create access token.
	token, err := utils.GenerateNewAccessToken()
	if err != nil {
		panic(err)
	}

	// Define a structure for specifying input and output data of a single test case.
	tests := []struct {
		description   string
		route         string // input route
		method        string // input method
		tokenString   string // input token
		body          io.Reader
		expectedError bool
		expectedCode  int
	}{
		{
			description:   "delete book without JWT and body",
			route:         "/api/v1/book",
			method:        "DELETE",
			tokenString:   "",
			body:          nil,
			expectedError: false,
			expectedCode:  400,
		},
		{
			description:   "delete book without right credentials",
			route:         "/api/v1/book",
			method:        "DELETE",
			tokenString:   "Bearer " + token,
			body:          strings.NewReader(dataString),
			expectedError: false,
			expectedCode:  403,
		},
		{
			description:   "delete book with credentials",
			route:         "/api/v1/book",
			method:        "DELETE",
			tokenString:   "Bearer " + token,
			body:          strings.NewReader(dataString),
			expectedError: false,
			expectedCode:  404,
		},
	}

	// Define a new Fiber app.
	app := fiber.New()

	// Define routes.
	PrivateRoutes(app)

	// Iterate through test single test cases
	for _, test := range tests {
		// Create a new http request with the route from the test case.
		req := httptest.NewRequest(test.method, test.route, test.body)
		req.Header.Set("Authorization", test.tokenString)
		req.Header.Set("Content-Type", "application/json")

		// Perform the request plain with the app.
		resp, err := app.Test(req, -1) // the -1 disables request latency

		// Verify, that no error occurred, that is not expected
		assert.Equalf(t, test.expectedError, err != nil, test.description)

		// As expected errors lead to broken responses,
		// the next test case needs to be processed.
		if test.expectedError {
			continue
		}

		// Verify, if the status code is as expected.
		assert.Equalf(t, test.expectedCode, resp.StatusCode, test.description)
	}
}

// ...
```

[↑ Table of contents](#toc)

<a name="#run-project-locally"></a>
## Run project locally

Let's run Docker containers, apply migrations and go to `http://127.0.0.1:5000/swagger/index.html`:

![Result](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cid2egkowu1vr4nd3ck0.png)

It works. Woohoo! 🎉

[↑ Table of contents](#toc)

<a name="#a-self-check-block-of-knowledge"></a>
## A self-check block of knowledge

Try not to peek at the text of the tutorial and answer as **quickly** and **honestly** as possible. Don't worry if you forgot something! Just keep going:

- What is the name of the technology that allows applications to run in an isolated environment?
- Where should the application's business logic be located (folder name)?
- What file should be created in the root of the project to describe the process of creating the container for the application?
- What is UUID and why do we use it for ID?
- What type of PostgreSQL field did we use to create the model for book attributes?
- Why is it better to use pure SQL in Go apps?
- Where do you need to describe the API method for auto-generating documentation (by Swagger)?
- Why separate configurations in testing?

[↑ Table of contents](#toc)

<a name="#plan-for-further-development"></a>
## Plan for further development

For further (_independent_) development of this application, I recommend considering the following options:

1. Upgrade the `CreateBook` method: add a handler to save picture to a cloud storage service (e.g., _Amazon S3 or similar_) and save only picture ID to our database;
2. Upgrade the `GetBook` and `GetBooks` methods: add a handler to change picture ID from a cloud service to direct link to this picture;
3. Add a new method for registering new users (e.g., _registered users can get a role, which will allow them to perform some methods in the REST API_);
4. Add a new method for user authorization (e.g., _after authorization, users receive a JWT token that contains credentials according to its role_);
5. Add a standalone container with Redis (or similar) to store the sessions of these authorized users;

[↑ Table of contents](#toc)

<a name=""></a>
## Photos by

- Joshua Woroniecki https://unsplash.com/photos/LeleZeefJ7M
- Maxwell Nelson https://unsplash.com/photos/Y9w872CNIyI
- Vic Shóstak https://shostak.dev

<a name=""></a>
## P.S.

If you want more articles (like this) on this blog, then post a comment below and subscribe to me. Thanks! 😻

And of course, you can help me make developers' lives even better! Just connect to one of my projects as a contributor. It's easy!

My projects that need your help (and stars) 👇

- 🔥 [gowebly][gowebly_url]: A next-generation CLI tool for easily build amazing web applications with Go on the backend, using htmx & hyperscript and the most popular atomic/utility-first CSS frameworks on the frontend.
- ✨ [create-go-app][cgapp_url]: Create a new production-ready project with Go backend, frontend and deploy automation by running one CLI command.
- 🏃 [yatr][yatr_url]: Yet Another Task Runner allows you to organize and automate your routine operations that you normally do in Makefile (or else) for each project.
- 📚 [gosl][gosl_url]: The Go Snippet Library provides snippets collection for working with routine operations in your Go programs with a super user-friendly API and the most efficient performance.
- 🏄‍♂️ [csv2api][csv2api_url]: The parser reads the CSV file with the raw data, filters the records, identifies fields to be changed, and sends a request to update the data to the specified endpoint of your REST API.
- 🚴 [json2csv][json2csv_url]: The parser can read given folder with JSON files, filtering and qualifying input data with intent & stop words dictionaries and save results to CSV files by given chunk size.

<!-- Footer links -->

[gowebly_url]: https://github.com/gowebly
[cgapp_url]: https://github.com/create-go-app
[yatr_url]: https://github.com/koddr/yatr
[gosl_url]: https://github.com/koddr/gosl
[csv2api_url]: https://github.com/koddr/csv2api
[json2csv_url]: https://github.com/koddr/json2csv