We use cookies and other tracking technologies to improve your browsing experience on our site, analyze site traffic, and understand where our audience is coming from. To find out more, please read our privacy policy.

By choosing 'I Accept', you consent to our use of cookies and other tracking technologies.

We use cookies and other tracking technologies to improve your browsing experience on our site, analyze site traffic, and understand where our audience is coming from. To find out more, please read our privacy policy.

By choosing 'I Accept', you consent to our use of cookies and other tracking technologies. Less

We use cookies and other tracking technologies... More

Login or register
to apply for this job!

Login or register
to publish this job!

Login or register
to save this job!

Login or register
to save interesting jobs!

Login or register
to get access to all your job applications!

Login or register to start contributing with an article!

Login or register
to see more jobs from this company!

Login or register
to boost this post!

Show some love to the author of this blog by giving their post some rocket fuel πŸš€.

Login or register to search for your ideal job!

Login or register to start working on this issue!

Login or register
to save articles!

Engineers who find a new job through Golang Works average a 15% increase in salary πŸš€

Blog hero image

Writing a Resilient and Scalable RESTful API in Go [Part 1]

Adeshina Hammed Hassan 21 April, 2021 | 5 min read

Introduction

In this blog, we are going to learn about a very scalable approach and structure that can be adopted to write REST service in Go. First, it is important to note that, there are many design patter one can adopt while writing a REST service in Go. The pattern we are going to adopt in this blog has proved to be very scalable and absolutely maintainable.

TL;DR: Here is the repository (main branch): github.com/D-sense/go-scalable-rest-api-example

The REST service we are going to build is a Library System, and we want to keep the number of endpoints/resources minimal and simple; the API will cater to books, authors, and customers. Since the point of this blog is to focus on a scalable and maintainable structure of the service, we will not be implementing customers’ authentication and authorization in this blog (there will be another blog written entirely for that). With that said, let us dive in.

Let’s break our whole architecture into four phases:

  • Phase 1: Defining the absolute database features/services. In this part, we will be defining the interactions with the database (CRUD).
  • Phase 2: Implementing the defined features.
  • Phase 3: Defining Handlers that call the implemented database features/services.
  • Phase 4: Passing the Handlers to Server (it could be JSON or GraphQL). The server will serve the resources to the API consumer.

Without wasting time, let us start with the implementation:

  • Phase 1: Defining the absolute features:

In this phase, we define the interface for features we need for authors, books, and customers. These features are simply the way we create, get, update, delete, or look for the specific record(s) in the database:

// In objects/author.go, we have:
type AuthorService interface {
	Create(ctx context.Context, data *Author) error
	Authors(ctx context.Context) ([]Author, error)
	Author(ctx context.Context, id string) (Author, error)
	FindAuthorByEmail(ctx context.Context, email string) (*Author, error)
}

// In objects/customer.go, we have:
type CustomerService  interface {
	Create(ctx context.Context, data Customer) error
	Customers(ctx context.Context) ([]*Customer, error)
	Customer(ctx context.Context, id string) (*Customer, error)
	FindCustomerByEmail(ctx context.Context, email string) (*Customer, error)
}

// In objects/book.go, we have:
type BookService interface {
	CreateBook(ctx context.Context, book Book) error
	Books(ctx context.Context) ([]*Book, error)
	Book(ctx context.Context, id string) (*Book, error)
	Delete(ctx context.Context, id string) error
	Update(ctx context.Context, data *Book) error
}

type Author struct {
   // fields
}
type Book struct {
  // fields
}
type Customer struct {
  // fields
}

We should now have this structure below:

 |── objects
  └── author.go.go
  └── customer.go
  └── book.go
  • Phase 2: Implementing the defined features:
    // In database/postgres/author_service.go, we have: 
    func CreateAuthor(context.Context, data *Author) error {
      // logic goes here
    }
    
    func Authors(context.Context) ([]Author, error) {
      // logic goes here
    }
    
    func Author(context.Context, id string) (Author, error) {
      // logic goes here
    }
    
    // In database/postgres/customer_service.go, we have: 
    func CreateCustomer(context.Context, data *Customer) error {
      // logic goes here
    }
    
    func Customers(context.Context) ([]Customer, error) {
      // logic goes here
    }
    
    func Customer(context.Context, id string) (Author, error) {
      // logic goes here
    }
    
    // In database/postgres/book_service.go, we have: 
    func CreateBook(context.Context, data *Book) error {
      // logic goes here
    }
    
    func Customers(context.Context) ([]Book, error) {
      // logic goes here
    }
    
    func Customer(context.Context, id string) (Book, error) {
      // logic goes here
    }
    
    func Delete(context.Context, id string) error {
      // logic goes here
    }
    
    func Update(context.Context, data *Book) (Book, error ){
      // logic goes here
    }
    
    
We should now have this structure below:
  |── database
    └── postgres
      └── author_service.go
      └── customer_service.go
      └── book_service.go
  • Phase 3: Defining the Handlers in which we can call the implemented Features.

In this phase, we define a handler for each of the defined features in Phase 2. Inside each handler, you may perform many things such as validation of data (before it is passed to the storage; we shall see this in a bit), logging, tracing, and more importantly, injecting and calling service(s) from within:

type Handler struct {
	authorService objects.AuthorService
	bookService   objects.BookService
	// add more services, such as Email Delivery service, Session Service, Third-parties services...as required.
}

// NewHandler is the Author handler constructor
func NewHandler(
	authorService objects.AuthorService,
	bookService objects.BookService,
) *Handler {
	h := &Handler{
		authorService: authorService,
		bookService:   bookService,
	}
	return h
}

// Example of Signup function can be defined as below:
func (h *Handler) SignUp(ctx *gin.Context, input *objects.RegistrationVM) (*objects.Author, error) {
	// input validation should be handled first.
	// Handle it yourself
	//

	pHashed, err := password.NewHashedPassword(input.Password)
	if err != nil {
		return nil, errors.Wrap(err, fmt.Sprintf("error hasshng a password"))
	}

	// do some checking such as if the email address already exists
	// Handle it yourself
	//

	author := &objects.Author{
		FullName: input.FullName,
		Email:    input.Email,
		Password: pHashed,
	}

        // here we are calling upon the author database service
	err = h.authorService.Create(ctx, author)
	if err != nil {
		return nil, errors.Wrap(err, fmt.Sprintf("error creating an author"))
	}

	return author, nil
}

We should now have this structure below:

 |── handlers
   └── author
        └── handler.go
   └── customer
        └── handler.go
   └── book
         └── handler.go
  • Phase 4: Passing the Handlers to Server (it could be JSON or GraphQL):

At this phase, we will be passing the Handlers (the ones we created at Phase 3) to the Server (of course, the Server could be JSON or GraphQL). As mentioned earlier, the Server will serve the resources to the API consumer. In this tutorial, we will be using a JSON format to expose the resources.

We should now have this structure below:

|── server
 └── json
     └── author.go
     └── book.go
     └── costumer.go 

Join our newsletter
Join over 111,000 others and get access to exclusive content, job opportunities and more!

The benefits:

Now that we are done designing and developing our REST API using the Service Pattern architecture/approach, let’s think of the scalability and maintainability of the service. How do we scale and/maintain the application? Let’s say after the release, we decide to add new features because the business demands them! Because we have designed and crafted out our application in such a very maintainable approach, the task becomes seamless to achieve; simply go from phase 1 to phase 4.

Let’s assume we used an ORM package such as GORM in Phase 2 but now we have decided to re-write the implementation using pure SQL? As you can see, there is nothing stopping in our way; absolutely we do not need to tamper or modify any part of Phase 1, Phase 3, or Phase 4. We simply re-write Phase 2 (replace ORM implementation with pure SQL) and plug it back as it was and everything remains just fine.

Or in the case of Database, as we have been using Postgres, connecting to MySQL or any other database is as simple as modifying just our database connection function; nothing more.

Conclusion:

In this article, we explored a clean and scalable approach to apply when writing an API (it could be REST or GraphQL by the way) using Service Pattern and other techniques. We also learned about a very safe approach of connecting to a database by avoiding all sorts of issues such as race condition, unsafe thread, multiple connections. In the episode, we will be looking at implementing authentication and authorization around it.

In the next blog, we are going to learn about crafting an efficient and secured database as a data source with extensive discussion on the GORM and Migrate packages.

I hope this was an interesting read for you as it was for me whilst writing it.

Keep Go-ing :)

Author's avatar
Adeshina Hammed Hassan
Software Engineer --> Go/Flutter/JS/PHP
    dart
    Distributed Systems
    PHP
    Go
    HTML
    JavaScript
    CSS

Related Issues

cosmos / gaia
  • Started
  • 0
  • 4
  • Intermediate
  • Go
cosmos / gaia
  • Started
  • 0
  • 3
  • Intermediate
  • Go
cosmos / ibc
  • Open
  • 0
  • 0
  • Intermediate
  • TeX
cosmos / ibc
cosmos / ibc
  • Started
  • 0
  • 1
  • Intermediate
  • TeX
viebel / klipse-clj
viebel / klipse-clj
  • Started
  • 0
  • 4
  • Intermediate
  • Clojure
viebel / klipse
  • Started
  • 0
  • 1
  • Intermediate
  • Clojure
viebel / klipse
  • 1
  • 2
  • Intermediate
  • Clojure
viebel / klipse
  • Started
  • 0
  • 4
  • Intermediate
  • Clojure
  • $80

Get hired!

Sign up now and apply for roles at companies that interest you.

Engineers who find a new job through Golang Works average a 15% increase in salary.

Start with GitHubStart with Stack OverflowStart with Email