We're planting a tree for every job application! Click here toΒ learn more

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

Adeshina Hammed Hassan

21 Apr 2021

β€’

5 min read

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

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

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 :)

Did you like this article?

Adeshina Hammed Hassan

Software Engineer --> Go/Flutter/JS/PHP

See other articles by Adeshina

Related jobs

Title

The company

title

Remote

Title

The company

title

Remote

Title

The company

title

Remote

Title

The company

title

Remote

Related articles

JavaScript Functional Style Made Simple

JavaScript Functional Style Made Simple

Daniel Boros

β€’

12 Sep 2021

JavaScript Functional Style Made Simple

JavaScript Functional Style Made Simple

Daniel Boros

β€’

12 Sep 2021

CareersCompaniesSitemapFunctional WorksBlockchain WorksJavaScript WorksAI WorksGolang WorksJava WorksPython WorksRemote Works
email iconhello@works-hub.comUK flag

Ground Floor, Verse Building, 18 Brunswick Place, London, N1 6DZ

US flag

108 E 16th Street, New York, NY 10003

Subscribe to our newsletter

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

Β© 2021 WorksHub

Privacy PolicyDeveloped by WorksHub