DEV Community

Cover image for Restful API with Golang practical approach
Firdavs Kasymov
Firdavs Kasymov

Posted on

Restful API with Golang practical approach

In this tutorial, we would be creating a Restful API with a practical approach of clean architecture and native Golang without any frameworks.

There are lots of tutorials out there, for Restful API but the issue they are facing is they don't have a clear structure and use various frameworks.

Those tutorials depend on the existence of some frameworks of Golang. With every external framework being used in the project, it comes with great responsibility. I don't have anything against these frameworks, but rather I prefer to minimize the use of external frameworks and stick with standard packages of Golang.

When a beginner starts learning Golang, most of them jump straight away to the famous external frameworks, such as Gin, Beego, Fiber and many more. Frameworks are good to implement something fast, but the main issue is here that beginners are using frameworks without understanding the native approach that Golang provides out of the box. This approach leads to the developer being dependent on the specific framework. If you are sticking with frameworks, what would you do if these frameworks are being deprecated, have bugs or are not being supported anymore?

All of these frameworks use the native Golang codes behind them with an extra layer which makes them frameworks.

So my two cents here is to try to learn standard Golang packages this way you will not get lost in the world of frameworks, as Golang provides most of the required things out of the box.

The initial requirement before we start our tutorial is to make sure that you have the following installed setup on your computer:
1) Golang (https://go.dev/doc/install)
2) IDE of your choice VSCode or GoLand JetBrains.
3) Postgresql (https://www.postgresql.org/download/)
4) Insomnia (https://insomnia.rest/)
5) Golang-Migrate (https://github.com/golang-migrate/migrate/blob/master/cmd/migrate/README.md)

For the purpose of this tutorial, we will not be using Docker as explaining Docker and it is function is out of the context of this tutorial.

Initial setup

Create your folder and initialize the app:

mkdir rest-api
cd rest-api
git init
Enter fullscreen mode Exit fullscreen mode

Golang has a package manager which is handled by go.mod file. To generate this file, you must run the following command:
go mod init {Name Of Remote Repository}.

The convention to create a go.mod is to use the name of the remote repository of your app, in my case, it will be go mod init github.com/fir1/rest-api.

It is important to name it as a repository name because that is how Go manages dependencies when you execute the following command go get {PackageName}.

First, we need to create a project structure, before starting the actual implementation of Restful API.

I have already written a full blog regarding structuring a Golang application, you can read it in full by clicking here. (https://dev.to/firdavs_kasymov/a-practical-approach-to-structuring-golang-applications-1cc2)

So in this project, we would be following the same structure as it is explained in the previous blog.

Architecture

The architecture of our program will be looking like the following diagram:

Rest API

As is clear from the diagram we have isolation, between Repository, Model, Service and Delivery layers.

This approach has been taken by the rules of the clean architecture of Uncle Bob more info at https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html

With this pattern we can have a clean and easy-to-follow logic, independent of any layer, so your business rules simply don't know anything at all about the outside world. The below flow explains how the layers access each other.

Delivery (REST, gRPC) -> Business Service -> Repository -> Model.

As you can see that we are not exposing the repository directly to the Delivery layer, instead only our business service has access to it. And once we have received an API call we just expose the business service to the delivery layer, which I think is quite logical to do and easier to follow.

Creating Restful endpoints

For the purpose of this tutorial we will be creating a simple REST API for the TO-DO list application which has a business logic of Creating, Reading, Updating and Deleting the todo item, known as (CRUD).

We will be using some essential libraries to make the REST API so please execute the following commands:

go get github.com/gorilla/mux

go get github.com/sirupsen/logrus

go get github.com/jmoiron/sqlx

go get github.com/lib/pq

go get github.com/asaskevich/govalidator

go get github.com/joho/godotenv

go get github.com/kelseyhightower/envconfig
Enter fullscreen mode Exit fullscreen mode

Briefly, we will be explaining what each library does:

mux:
Package gorilla/mux implements a request router and dispatcher for matching incoming requests to their respective handler. Read in full from here (https://github.com/gorilla/mux)

logrus:
Logrus is a structured logger for Go (golang), completely API compatible with the standard library logger. (https://github.com/sirupsen/logrus)

sqlx:
sqlx is a library which provides a set of extensions on go's standard database/sql library. (https://github.com/jmoiron/sqlx)

pq:
A pure Go postgres driver for Go's database/sql package (https://github.com/lib/pq)

govalidator:
A package of validators and sanitisers for strings, structs and collections (https://github.com/asaskevich/govalidator)

godotenv:
A library for loading .env config files

envconfig:
Library for managing configuration data from environment variables (https://github.com/joho/godotenv)

Config

In this project, we are going to use dependencies such as Postgres, as Postgres requires the client connection to provide credentials to be able to connect to the database.
So we have decided to create .env file in the root of project with the credentials for the database we are going to use:

DATABASE_HOST=localhost
DATABASE_PORT=5432
DATABASE_USER=postgres
DATABASE_PASSWORD=password
DATABASE_NAME=rest
SERVER_PORT=80
Enter fullscreen mode Exit fullscreen mode

The above credentials for Database might be different in your local computer, so make sure to adjust it accordingly.

Create a file in directory configs/config.go with the content:

package configs

import (
    "github.com/joho/godotenv"
    "github.com/kelseyhightower/envconfig"
)

type Config struct {
    Database   Database
    ServerPort int `envconfig:"SERVER_PORT" default:"80"`
}

type Database struct {
    Host     string `envconfig:"DATABASE_HOST" required:"true"`
    Port     int    `envconfig:"DATABASE_PORT" required:"true"`
    User     string `envconfig:"DATABASE_USER" required:"true"`
    Password string `envconfig:"DATABASE_PASSWORD" required:"true"`
    Name     string `envconfig:"DATABASE_NAME" required:"true"`
}

func NewParsedConfig() (Config, error) {
    _ = godotenv.Load(".env")
    cnf := Config{}
    err := envconfig.Process("", &cnf)
    return cnf, err
}
Enter fullscreen mode Exit fullscreen mode

This code will be responsible for parsing .env files content to the struct Config.

Helper packages

In this section, we would be creating a helper package, which can be used within our project or can be imported to external projects.

Package: db
This package will have methods linked to the Database, such as connecting to the database, and error handling.

Create a folder in pkg/db which will include the files of db package.

Create the following associated files.

pkg/db/db.go

package db

import (
    "fmt"
    "github.com/jmoiron/sqlx"
    _ "github.com/lib/pq"
)

type ConfingDB struct {
    Host     string
    Port     int
    User     string
    Password string
    Name     string
}

func Connect(cnf ConfingDB) (*sqlx.DB, error) {
    dsn := fmt.Sprintf(
        "host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
        cnf.Host,
        cnf.Port,
        cnf.User,
        cnf.Password,
        cnf.Name,
    )
    db, err := sqlx.Connect("postgres", dsn)
    return db, err
}
Enter fullscreen mode Exit fullscreen mode

And the following file for error handling

pkg/db/error.go

package db

import (
    "database/sql"
    "errors"
    "fmt"
)

func HandleError(err error) error {
    if errors.Is(err, sql.ErrNoRows) {
        return ErrObjectNotFound{}
    }
    return err
}

// ErrObjectNotFound is used to indicate that selecting an individual object
// yielded no result. Declared as type, not value, for consistency reasons.
type ErrObjectNotFound struct{}

func (ErrObjectNotFound) Error() string {
    return "object not found"
}
func (ErrObjectNotFound) Unwrap() error {
    return fmt.Errorf("object not found")
}
Enter fullscreen mode Exit fullscreen mode

Database migration

If you have followed the initial setup, you should have already "golang-migrate" installed locally.

So we use migration files all together to keep the track of database scheme changes.

For the purpose of this tutorial, we must create a table named todo for our database so for that we would be using golang-migrate tool.
First, create a folder migrations in the root directory.

Next, execute the following command from the root working directory of the project

migrate create -ext sql -dir migrations -seq create_todo_table
Enter fullscreen mode Exit fullscreen mode

The command will automatically create two files inside migrations folder.

So we have now two files:

  • migrations/000001_create_todo_table.up.sql

We need to put all the changes which we would like to do in a database on this file, such as the creation of a new table or any changes related to the database.

Please put the following content into this file:

CREATE TABLE todo
(
    id SERIAL,
    name TEXT NOT NULL,
    description TEXT NOT NULL,
    status SMALLINT NOT NULL,
    created_on TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL,
    updated_on TIMESTAMP(0) WITHOUT TIME ZONE,
    deleted_on  TIMESTAMP(0) WITHOUT TIME ZONE,

    PRIMARY KEY (id)
);
Enter fullscreen mode Exit fullscreen mode

When using Migrate CLI we need to pass to database URL. Let's export it to a variable for convenience:

export POSTGRESQL_URL='postgres://postgres:password@localhost:5432/rest?sslmode=disable'
Enter fullscreen mode Exit fullscreen mode

Then run migration files by executing the command:

migrate -database ${POSTGRESQL_URL} -path migrations up
Enter fullscreen mode Exit fullscreen mode
  • 000001_create_todo_table.down.sql

In case of if you would like to roll back migrations, we will use .down.sql files, so in this tutorial rollback of migration would be to delete a created table.

So please put the following content into this file:

DROP TABLE IF EXISTS todo;
Enter fullscreen mode Exit fullscreen mode

To rollback the previously executed migration run the following command:

migrate -database ${POSTGRESQL_URL} -path migrations down
Enter fullscreen mode Exit fullscreen mode

Creating Layers

Model

We would be creating a separate folder for the model layer in the directory internal/todo/model/model.go inside the file please have the following content:

package model

import "time"

type Status int

const (
    StatusPending Status = iota + 1
    StatusInProgress
    StatusDone
)

func (s Status) IsValid() bool {
    switch s {
    case StatusPending:
        return true
    case StatusInProgress:
        return true
    case StatusDone:
        return true
    }
    return false
}

type ToDo struct {
    ID          int        `db:"id"`
    Name        string     `db:"name"`
    Description string     `db:"description"`
    Status      Status     `db:"status"`
    CreatedOn   time.Time  `db:"created_on"`
    UpdatedOn   *time.Time `db:"updated_on"`
    DeletedOn   *time.Time `db:"deleted_on"`
}

Enter fullscreen mode Exit fullscreen mode

We would be using model purely to interact with the Database, to get the object from it and parse it to our model.

Repository

Let's create our repository which will be responsible for handling all the database interactions for the ToDo model.

Create a separate directory for the repository internal/todo/repository
and have file repository.go within:

package repository

import (
    "context"
    "fmt"
    "github.com/fir1/rest-api/internal/todo/model"
    "github.com/fir1/rest-api/pkg/db"
    "github.com/jmoiron/sqlx"
)

type Repository struct {
    Db *sqlx.DB
}

func NewRepository(db *sqlx.DB) Repository {
    return Repository{Db: db}
}

func (r Repository) Find(ctx context.Context, id int) (model.ToDo, error) {
    entity := model.ToDo{}
    query := fmt.Sprintf(
        "SELECT * FROM todo WHERE id = $1 AND deleted_on IS NULL",
    )
    err := r.Db.GetContext(ctx, &entity, query, id)
    return entity, db.HandleError(err)
}

func (r Repository) Create(ctx context.Context, entity *model.ToDo) error {
    query := `INSERT INTO todo (name, description, status, created_on, updated_on)
                VALUES (:name, :description, :status, :created_on, :updated_on) RETURNING id;`
    rows, err := r.Db.NamedQueryContext(ctx, query, entity)
    if err != nil {
        return db.HandleError(err)
    }

    for rows.Next() {
        err = rows.StructScan(entity)
        if err != nil {
            return db.HandleError(err)
        }
    }
    return db.HandleError(err)
}

func (r Repository) Update(ctx context.Context, entity model.ToDo) error {
    query := `UPDATE todo
                SET name = :name, 
                    description = :description, 
                    status = :status, 
                    created_on = :created_on, 
                    updated_on = :updated_on, 
                    deleted_on = :deleted_on
                WHERE id = :id;`
    _, err := r.Db.NamedExecContext(ctx, query, entity)
    return db.HandleError(err)
}

func (r Repository) FindAll(ctx context.Context) ([]model.ToDo, error) {
    var entities []model.ToDo
    query := fmt.Sprintf(
        "SELECT * FROM todo WHERE deleted_on IS NULL",
    )
    err := r.Db.SelectContext(ctx, &entities, query)
    return entities, db.HandleError(err)
}
Enter fullscreen mode Exit fullscreen mode

The repository layer has only one purpose, it will make a SQL query to the Database. So in this case it provides the base CRUD operations for the ToDo model.

Create a separate directory for the service layer, so you can write the business logic here, so we would be creating a directory internal/todo/service and the files internal/todo/service/service.go and internal/todo/service/get.go within it:

Please have the following content on the file internal/todo/service/service.go

Service

We would be implementing the business logic for the CRUD operations. Please create the following folder in directory internal/todo/service and all the following files within the created directory

internal/todo/service/service.go

package service

import (
    "github.com/fir1/rest-api/internal/todo/repository"
)

type Service struct {
    repo repository.Repository
}

func NewService(r repository.Repository) Service {
    return Service{
        repo: r,
    }
}

Enter fullscreen mode Exit fullscreen mode

It returns an instance of service, and this instance would be having all methods of our business logic.

So in this tutorial, the Delivery Layer, can only call the Service layer to get access to the business logic.

Create
The business logic will validate the parameters of the function and save the ToDo in the database.

internal/todo/service/create.go

package service

import (
    "context"
    "github.com/asaskevich/govalidator"
    "github.com/fir1/rest-api/internal/todo/model"
    "github.com/fir1/rest-api/pkg/erru"
    "time"
)

type CreateParams struct {
    Name        string       `valid:"required"`
    Description string       `valid:"required"`
    Status      model.Status `valid:"required"`
}

func (s Service) Create(ctx context.Context, params CreateParams) (int, error) {
    if _, err := govalidator.ValidateStruct(params); err != nil {
        return 0, erru.ErrArgument{Wrapped: err}
    }

    tx, err := s.repo.Db.BeginTxx(ctx, nil)
    if err != nil {
        return 0, err
    }
    // Defer a rollback in case anything fails.
    defer tx.Rollback()

    entity := model.ToDo{
        Name:        params.Name,
        Description: params.Description,
        Status:      params.Status,
        CreatedOn:   time.Now().UTC(),
    }
    err = s.repo.Create(ctx, &entity)
    if err != nil {
        return 0, err
    }

    err = tx.Commit()
    return entity.ID, err
}

Enter fullscreen mode Exit fullscreen mode

It returns an instance of service, and this instance would be having all methods of our business logic. Please note that here we have a separate type CreateParams for taking arguments to function rather than using model.go. Because we would like to have clear isolation between each layer, so this way we would not clutter the tags of types and mandatory arguments to the function.

If we have used type ToDo from model.go as an argument to the function.

type ToDo struct {
    ID          int        `db:"id"`
    Name        string     `db:"name"`
    Description string     `db:"description"`
    Status      Status     `db:"status"`
    CreatedOn   time.Time  `db:"created_on"`
    UpdatedOn   *time.Time `db:"updated_on"`
    DeletedOn   *time.Time `db:"deleted_on"`
}
Enter fullscreen mode Exit fullscreen mode

Which fields from ToDo model would be mandatory for the business logic?

As it is clear that it will mess up our architecture if we use the model.go types as arguments to the business layer. That is one of the reasons why I like to isolate the layers, so we have a clear understanding of mandatory fields for the functions.

Get

The business logic here is quite straightforward, we will have a function which will take id as a mandatory parameter then we would be looking up the database.

internal/todo/service/get.go


package service

import (
    "context"
    "errors"
    "github.com/fir1/rest-api/internal/todo/model"
    "github.com/fir1/rest-api/pkg/db"
    "github.com/fir1/rest-api/pkg/erru"
)

func (s Service) Get(ctx context.Context, id int) (model.ToDo, error) {
    todo, err := s.repo.Find(ctx, id)
    switch {
    case err == nil:
    case errors.As(err, &db.ErrObjectNotFound{}):
        return model.ToDo{}, erru.ErrArgument{errors.New("todo object not found")}
    default:
        return model.ToDo{}, err
    }
    return todo, nil
}

Enter fullscreen mode Exit fullscreen mode

Update

The business logic here is that we will be taking id as a mandatory field, so we can look up the ToDo entity from the Database, then update the fields name, description, status optionally if it is provided.

internal/todo/service/update.go


package service

import (
    "context"
    "errors"
    "github.com/asaskevich/govalidator"
    "github.com/fir1/rest-api/internal/todo/model"
    "github.com/fir1/rest-api/pkg/erru"
)

type UpdateParams struct {
    ID          int `valid:"required"`
    Name        *string
    Description *string
    Status      *model.Status
}

func (s Service) Update(ctx context.Context, params UpdateParams) error {
    if _, err := govalidator.ValidateStruct(params); err != nil {
        return erru.ErrArgument{Wrapped: err}
    }

    // find todo object
    todo, err := s.Get(ctx, params.ID)
    if err != nil {
        return err
    }

    if params.Name != nil {
        todo.Name = *params.Name
    }
    if params.Description != nil {
        todo.Description = *params.Description
    }
    if params.Status != nil {
        if !params.Status.IsValid() {
            return erru.ErrArgument{Wrapped: errors.New("given status not valid")}
        }
        todo.Status = *params.Status
    }

    tx, err := s.repo.Db.BeginTxx(ctx, nil)
    if err != nil {
        return err
    }
    // Defer a rollback in case anything fails.
    defer tx.Rollback()

    err = s.repo.Update(ctx, todo)
    if err != nil {
        return err
    }

    err = tx.Commit()
    return err
}

Enter fullscreen mode Exit fullscreen mode

Delete

Here we would be finding the ToDo entity from the database, if it is found then we soft deleted it by putting the current time for the field DeletedOn.
I choose to soft delete the records compared to hard delete from the database table, because this way we would be having records which we can use for various purposes, such as auditing and etc.

internal/todo/service/delete.go

package service

import (
    "context"
    "time"
)

func (s Service) Delete(ctx context.Context, id int) error {
    todo, err := s.Get(ctx, id)
    if err != nil {
        return err
    }

    tx, err := s.repo.Db.BeginTxx(ctx, nil)
    if err != nil {
        return err
    }
    // Defer a rollback in case anything fails.
    defer tx.Rollback()

    now := time.Now().UTC()
    todo.DeletedOn = &now
    err = s.repo.Update(ctx, todo)
    if err != nil {
        return err
    }

    err = tx.Commit()
    return err
}

Enter fullscreen mode Exit fullscreen mode

Delivery

Here we would be finding the ToDo entity from the database, if it is found then we soft deleted it by putting the current time for the field DeletedOn.
I choose to soft delete the records compared to hard delete from the database table, because this way we would be having records which we can use for various purposes, such as auditing and etc. In this section, we would be implementing the delivery layer, with the Restful server.

So please create a folder in the directory http/rest.

As we have described CRUD, so for each functionality of CRUD we would be having a separate handler which will receive external API calls, and then each handler will call it is relevant business service. We would be creating a separate handler and explaining the logic.

Initiate handler service

First we must initiate a handler service, so please create a file in http/rest/handlers/handler.go and put the following content:

package handlers

import (
    toDoRepo "github.com/fir1/rest-api/internal/todo/repository"
    toDoService "github.com/fir1/rest-api/internal/todo/service"
    "github.com/gorilla/mux"
    "github.com/jmoiron/sqlx"
    "github.com/sirupsen/logrus"
)

type service struct {
    logger      *logrus.Logger
    router      *mux.Router
    toDoService toDoService.Service
}

func newHandler(lg *logrus.Logger, db *sqlx.DB) service {
    return service{
        logger:      lg,
        toDoService: toDoService.NewService(toDoRepo.NewRepository(db)),
    }
}

Enter fullscreen mode Exit fullscreen mode

The handler service will take business service as a dependency argument in our case the dependency is toDoService which is our internal business service layer.

Helpers

There is common logic which is being used by every handler, so I have decided to put that logic in a separate helper function, so we can reuse it rather than duplicate it.

Create a file in http/rest/handlers/helper.go and put the following content.

package handlers

import (
    "bytes"
    "encoding/json"
    "errors"
    "github.com/fir1/rest-api/pkg/erru"
    "io"
    "net/http"
)

/*
Don’t have to repeat yourself every time you respond to user, instead you can use some helper functions.
*/
func (s service) respond(w http.ResponseWriter, data interface{}, status int) {
    var respData interface{}
    switch v := data.(type) {
    case nil:
    case erru.ErrArgument:
        status = http.StatusBadRequest
        respData = ErrorResponse{ErrorMessage: v.Unwrap().Error()}
    case error:
        if http.StatusText(status) == "" {
            status = http.StatusInternalServerError
        } else {
            respData = ErrorResponse{ErrorMessage: v.Error()}
        }
    default:
        respData = data
    }

    w.WriteHeader(status)
    w.Header().Set("Content-Type", "application/json")
    if data != nil {
        err := json.NewEncoder(w).Encode(respData)
        if err != nil {
            http.Error(w, "Could not encode in json", http.StatusBadRequest)
            return
        }
    }
}

// it does not read to the memory, instead it will read it to the given 'v' interface.
func (s service) decode(r *http.Request, v interface{}) error {
    return json.NewDecoder(r.Body).Decode(v)
}

// it reads to the memory.
func (s service) readRequestBody(r *http.Request) ([]byte, error) {
    // Read the content
    var bodyBytes []byte
    var err error
    if r.Body != nil {
        bodyBytes, err = io.ReadAll(r.Body)
        if err != nil {
            err := errors.New("could not read request body")
            return nil, err
        }
    }
    return bodyBytes, nil
}

// will place the body bytes back to the request body which could be read in subsequent calls on Handlers
// for example, you have more than 1 middleware and each of them need to read the body. If the first middleware read the body
// the second one won't be able to read it, unless you put the request body back.
func (s service) restoreRequestBody(r *http.Request, bodyBytes []byte) {
    // Restore the io.ReadCloser to its original state
    r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
}

Enter fullscreen mode Exit fullscreen mode

Create
We will be creating handlers and the practical approach would be to have a struct for the request, response within the relevant handler, this way we have clear isolation between the data which is being required from the API call, and the data which is being inserted into the business layer, rather than using model everywhere within our application.

Some of you might not agree with me but I prefer clean code over a complicated one.
The main important thing for developers is to write the code clean, so the fellow developers can understand easily.

http/rest/handlers/create.go

package handlers

import (
    "github.com/fir1/rest-api/internal/todo/model"
    toDoService "github.com/fir1/rest-api/internal/todo/service"
    "net/http"
)

func (s service) Create() http.HandlerFunc {
    type request struct {
        Name        string       `json:"name"`
        Description string       `json:"description"`
        Status      model.Status `json:"status"`
    }

    type response struct {
        ID int `json:"id"`
    }

    return func(w http.ResponseWriter, r *http.Request) {
        req := request{}
        // Try to decode the request body into the struct. If there is an error,
        // respond to the client with the error message and a 400 status code.
        err := s.decode(r, &req)
        if err != nil {
            s.respond(w, err, 0)
            return
        }

        id, err := s.toDoService.Create(r.Context(), toDoService.CreateParams{
            Name:        req.Name,
            Description: req.Description,
            Status:      req.Status,
        })
        if err != nil {
            s.respond(w, err, 0)
            return
        }
        s.respond(w, response{ID: id}, http.StatusOK)
    }
}

Enter fullscreen mode Exit fullscreen mode

As you can see the job of our handler is to receive an API call, and then call the relevant business service layer, to handle the rest of the logic (s.toDoService.Create - this is our business service layer).

It is a clean approach, as our handler knows only about the business service layer, and the handler does not know anything about the underlying logic of the business.

This approach is practical because imagine if you would like to expose your API with gRPC protocol, with the current approach we just add an additional delivery layer in the directory http/grpc then call our business service layer, without the need to write any business logic.

Get

Please create a file in http/rest/handlers/get.go and have the following content:

package handlers

import (
    "errors"
    "github.com/fir1/rest-api/internal/todo/model"
    "github.com/fir1/rest-api/pkg/erru"
    "github.com/gorilla/mux"
    "net/http"
    "strconv"
    "time"
)

func (s service) Get() http.HandlerFunc {
    type response struct {
        ID          int          `json:"id"`
        Name        string       `json:"name"`
        Description string       `json:"description"`
        Status      model.Status `json:"status"`
        CreatedOn   time.Time    `json:"created_on"`
        UpdatedOn   *time.Time   `json:"updated_on,omitempty"`
    }
    return func(w http.ResponseWriter, r *http.Request) {
        vars := mux.Vars(r)

        id, err := strconv.Atoi(vars["id"])
        if err != nil {
            s.respond(w, erru.ErrArgument{
                Wrapped: errors.New("valid id must provide in path"),
            }, 0)
            return
        }

        getResponse, err := s.toDoService.Get(r.Context(), id)
        if err != nil {
            s.respond(w, err, 0)
            return
        }
        s.respond(w, response{
            ID:          getResponse.ID,
            Name:        getResponse.Name,
            Description: getResponse.Description,
            Status:      getResponse.Status,
            CreatedOn:   getResponse.CreatedOn,
            UpdatedOn:   getResponse.UpdatedOn,
        }, http.StatusOK)
    }
}
Enter fullscreen mode Exit fullscreen mode

Update

Please create a file in http/rest/handlers/update.go and have the following content:

package handlers

import (
    "errors"
    "github.com/fir1/rest-api/internal/todo/model"
    toDoService "github.com/fir1/rest-api/internal/todo/service"
    "github.com/fir1/rest-api/pkg/erru"
    "github.com/gorilla/mux"
    "net/http"
    "strconv"
)

func (s service) Update() http.HandlerFunc {
    type request struct {
        Name        *string       `json:"name"`
        Description *string       `json:"description"`
        Status      *model.Status `json:"status"`
    }

    type response struct {
        ID int `json:"id"`
    }

    return func(w http.ResponseWriter, r *http.Request) {
        vars := mux.Vars(r)
        id, err := strconv.Atoi(vars["id"])
        if err != nil {
            s.respond(w, erru.ErrArgument{
                Wrapped: errors.New("valid id must provide in path"),
            }, 0)
            return
        }

        req := request{}
        // Try to decode the request body into the struct. If there is an error,
        // respond to the client with the error message and a 400 status code.
        err = s.decode(r, &req)
        if err != nil {
            s.respond(w, err, 0)
            return
        }

        err = s.toDoService.Update(r.Context(), toDoService.UpdateParams{
            ID:          id,
            Name:        req.Name,
            Description: req.Description,
            Status:      req.Status,
        })
        if err != nil {
            s.respond(w, err, 0)
            return
        }
        s.respond(w, response{ID: id}, http.StatusOK)
    }
}

Enter fullscreen mode Exit fullscreen mode

Delete
Please create a file in http/rest/handlers/delete.go and have the following content:

package handlers

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

func (s service) Delete() http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        vars := mux.Vars(r)

        id, err := strconv.Atoi(vars["id"])
        if err != nil {
            s.respond(w, err, 0)
            return
        }

        err = s.toDoService.Delete(r.Context(), id)
        if err != nil {
            s.respond(w, err, 0)
            return
        }
        s.respond(w, nil, http.StatusOK)
    }
}
Enter fullscreen mode Exit fullscreen mode

Middleware
As you might have noticed by this time that one of the most important parts of every application is being able to log, request and response to API calls.
Disclaimer, when you log requests and responses you must be careful not to log sensitive information such as credentials, usernames, passwords and any other data which violates privacy.

Please create a file in http/rest/middleware_logger.go and put the following content inside it:

package handlers

import (
    "fmt"
    "net/http"
    "time"
)

// responseWriter is a minimal wrapper for http.ResponseWriter that allows the
// written HTTP status code to be captured for logging. This type will implement http.ResponseWriter.
type responseWriter struct {
    http.ResponseWriter
    status      int
    body        []byte
    wroteHeader bool
    wroteBody   bool
}

func wrapResponseWriter(w http.ResponseWriter) *responseWriter {
    return &responseWriter{ResponseWriter: w}
}

func (rw *responseWriter) Status() int {
    return rw.status
}

func (rw *responseWriter) WriteHeader(code int) {
    if rw.wroteBody {
        return
    }
    rw.status = code
    rw.ResponseWriter.WriteHeader(code)
    rw.wroteHeader = true
}

func (rw *responseWriter) Write(body []byte) (int, error) {
    if rw.wroteBody {
        return 0, nil
    }
    i, err := rw.ResponseWriter.Write(body)
    if err != nil {
        return 0, err
    }
    rw.body = body
    return i, err
}

func (rw *responseWriter) Body() []byte {
    return rw.body
}

// middlewarMiddlewareLoggereLogger logs the incoming HTTP request and response. Enable it only for debug purpose disable it on production.
func (s service) MiddlewareLogger() func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        fn := func(w http.ResponseWriter, r *http.Request) {
            if r.URL.Path == "/healthz" {
                // Call the next handler don't log if it is internal request from health check of Kubernetes
                next.ServeHTTP(w, r)
                return
            }

            defer func() {
                if err := recover(); err != nil {
                    w.WriteHeader(http.StatusInternalServerError)
                }
            }()

            requestBody, err := s.readRequestBody(r)
            if err != nil {
                s.respond(w, err, 0)
                return
            }
            s.restoreRequestBody(r, requestBody)

            logMessage := fmt.Sprintf("path:%s, method: %s, requestBody: %v", r.URL.EscapedPath(), r.Method, string(requestBody))

            start := time.Now()
            wrapped := wrapResponseWriter(w)
            next.ServeHTTP(wrapped, r)

            logMessage = fmt.Sprintf("%s, responseStatus: %d, responseBody: %s", logMessage, wrapped.Status(), string(wrapped.Body()))
            s.logger.Infof("%s, duration: %v", logMessage, time.Since(start))
        }
        return http.HandlerFunc(fn)
    }
}
Enter fullscreen mode Exit fullscreen mode

Register Handlers

As of now, we have all the handlers with the relevant handlers, but we need to register those handlers to the router with the customized Path.
We would be creating a file in http/rest/handlers/routes.go and have the following content:

package handlers

import (
    "github.com/gorilla/mux"
    "github.com/jmoiron/sqlx"
    "github.com/sirupsen/logrus"
    "net/http"
)

func Register(r *mux.Router, lg *logrus.Logger, db *sqlx.DB) {
    handler := newHandler(lg, db)
    // adding logger middleware
    r.Use(handler.MiddlewareLogger())
    r.HandleFunc("/healthz", handler.Health())
    r.HandleFunc("/todo", handler.Create()).Methods(http.MethodPost)
    r.HandleFunc("/todo/{id}", handler.Get()).Methods(http.MethodGet)
    r.HandleFunc("/todo/{id}", handler.Update()).Methods(http.MethodPut)
    r.HandleFunc("/todo/{id}", handler.Delete()).Methods(http.MethodDelete)
}
Enter fullscreen mode Exit fullscreen mode

Creating Server

In this stage, we need to create an instance of an actual server which can receive API calls and handle those calls.

Please create a file in http/rest/logger.go and have the following content inside it:

package rest

import (
    "github.com/sirupsen/logrus"
    "os"
)

func NewLogger() *logrus.Logger {
    log := logrus.New()
    log.SetOutput(os.Stdout)
    log.SetLevel(logrus.InfoLevel)
    log.SetFormatter(&logrus.TextFormatter{
        ForceColors:     true,
        TimestampFormat: "2006-01-02 15:04:05.999999999",
        FullTimestamp:   true,
    })
    return log
}

Enter fullscreen mode Exit fullscreen mode

Create a file in http/rest/server.go and have the following content inside it:

package rest

import (
    "context"
    "fmt"
    "github.com/fir1/rest-api/configs"
    "github.com/fir1/rest-api/http/rest/handlers"
    "github.com/fir1/rest-api/pkg/db"
    "github.com/gorilla/mux"
    "github.com/rs/cors"
    "github.com/sirupsen/logrus"
    "net/http"
    "os"
    "os/signal"
    "sync"
    "syscall"
)

type Server struct {
    logger *logrus.Logger
    router *mux.Router
    config configs.Config
}

func NewServer() (*Server, error) {
    cnf, err := configs.NewParsedConfig()
    if err != nil {
        return nil, err
    }

    database, err := db.Connect(db.ConfingDB{
        Host:     cnf.Database.Host,
        Port:     cnf.Database.Port,
        User:     cnf.Database.User,
        Password: cnf.Database.Password,
        Name:     cnf.Database.Name,
    })
    if err != nil {
        return nil, err
    }

    log := NewLogger()
    router := mux.NewRouter()
    handlers.Register(router, log, database)

    s := Server{
        logger: log,
        config: cnf,
        router: router,
    }
    return &s, nil
}

func (s *Server) Run(ctx context.Context) error {
    server := http.Server{
        Addr:    fmt.Sprintf(":%d", s.config.ServerPort),
        Handler: cors.Default().Handler(s.router),
    }

    stopServer := make(chan os.Signal, 1)
    signal.Notify(stopServer, syscall.SIGINT, syscall.SIGTERM)

    defer signal.Stop(stopServer)

    // channel to listen for errors coming from the listener.
    serverErrors := make(chan error, 1)
    var wg sync.WaitGroup
    wg.Add(1)
    go func(wg *sync.WaitGroup) {
        defer wg.Done()
        s.logger.Printf("REST API listening on port %d", s.config.ServerPort)
        serverErrors <- server.ListenAndServe()
    }(&wg)

    // blocking run and waiting for shutdown.
    select {
    case err := <-serverErrors:
        return fmt.Errorf("error: starting REST API server: %w", err)
    case <-stopServer:
        s.logger.Warn("server received STOP signal")
        // asking listener to shutdown
        err := server.Shutdown(ctx)
        if err != nil {
            return fmt.Errorf("graceful shutdown did not complete: %w", err)
        }
        wg.Wait()
        s.logger.Info("server was shut down gracefully")
    }
    return nil
}

func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    s.router.ServeHTTP(w, r)
}
Enter fullscreen mode Exit fullscreen mode

Run Server

At this point now we can run a Restful server, and test all our integrations.

Please create a file in cmd/app/main.go and have the following content inside it:

package main

import (
    "context"
    "github.com/fir1/rest-api/http/rest"
    "log"
)

func main() {
    if err := run(context.Background()); err != nil {
        log.Fatalf("%+v", err)
    }
}

func run(ctx context.Context) error {
    server, err := rest.NewServer()
    if err != nil {
        return err
    }
    err = server.Run(ctx)
    return err
}

Enter fullscreen mode Exit fullscreen mode

Our main package has a few lines of code, which is an entry point to our application. I think the main package should be clean and must be used to start servers without any extra logic.

Tests

In this section, we are going to test our API server manually by making a Restful API call with the help of the Insomnia Tool.

Make sure that you are in the root directory and run the following command from the terminal go run cmd/app/*.go

Create

Make an API call to the

POST localhost:80/todo

Request Body

{
  "name": "Tutorial Restful pending",
  "description": "Still we are testing Restful api so not finished",
  "status": 1
}

Enter fullscreen mode Exit fullscreen mode

Rest API

Get

Make an API call to the

GET localhost:80/todo/4 (id of todo - 4 might be different in your case)

Enter fullscreen mode Exit fullscreen mode

Rest API

Update

Make an API call to the

PUT localhost:80/todo/4 (id of todo - 4 might be different in your case)

Request Body
{
  "name": "Tutorial Restful finished",
  "description": "Restful api testing finished",
  "status": 2
}

Enter fullscreen mode Exit fullscreen mode

Rest API

Get Updated ToDo

Make an API call to the

GET localhost:80/todo/4 (id of todo - 4 might be different in your case)
Enter fullscreen mode Exit fullscreen mode

Rest API

From the above screenshot, it is clear that the entity ToDo was updated.

Delete

Make an API call to the

DELETE localhost:80/todo/4 (id of todo - 4 might be different in your case)
Enter fullscreen mode Exit fullscreen mode

Rest API

The entity was deleted, so when we do GET request we will be getting an error, as the object does not exist anymore.

Enhancements

You can even enhance this project by adding more functionalities, such as:

  • Add todo list endpoint, that will return all existing ToDo's entity.
  • Extra functionalities, such as filtering the ToDo by status and keywords.
  • Add authentication

Once you have done these enhancements you can create a PR towards my repository github.com/fir1/rest-api

The end

Hopefully, this tutorial was useful and I would be more than happy to receive questions or suggestions about it.

The source code for this tutorial can be found here

That’s it for this blog. Please feel free to comment with your views on this blog. Thanks for your time reading this blog and hope it was useful.

Happy learning and sharing.

Top comments (8)

Collapse
 
rajkumarmano profile image
Rajkumar

Very useful.

Collapse
 
sxmmie profile image
Samuel Umoh

Great stuff

Collapse
 
imjoseangel profile image
Jose Angel Munoz

Thank you!! Good reference!!

Collapse
 
midir99 profile image
midir99

Great job man, I find it very useful!

Collapse
 
firdavs_kasymov profile image
Firdavs Kasymov

Happy that you found it useful :)

Collapse
 
go-lang-developer profile image
sunv

First I saw basic tutorial at youtu.be/WOtiPX-iiHU and then i implemented code using this blog it makes life easy.. thank you So much

Collapse
 
appdev profile image
@13x

Can i ask how to call the postgresql function in golang

Collapse
 
firdavs_kasymov profile image
Firdavs Kasymov

Please see the REPOSITORY layer which explains the usage of POSTGRESQL.