REST API using GO(gin) & Mongo
Tags: go, mongodb, rest, api
Published: 2023-06-27 04:31:36

In this blog, we will be building a RESTful API server with two endpoints. The example project will be a repository of data about books in a library.

This includes the following sections:
1. Design API endpoints.
2. Create a folder for code.
3. Connect to MongoDB using qmgo
4. Write handlers to
  - add a new book
  - get all books
  - get a specific book
  - delete a specific book


## Prerequisites:

- An installation of Go 1.20 or later. For installation instructions, [visit](https://go.dev/doc/install).
- An installation of MongoDB 6.0 or later. For installation
instructions, [visit](https://www.mongodb.com/docs/manual/installation/) 
- Any text editor or IDE we have will work fine.


## Design API endpoints:

We’ll build an API that provides access to the books in a library. So we’ll need to provide endpoints through which a client can get, add or delete books in a library.


Here are the endpoints we’ll create in this tutorial.

`/books`

GET – Get a list of all books, returned as JSON.
POST – Add a new book from the request data sent as JSON.

`/books/:id`

GET – Get a book by its ID, returning the book data as JSON.
PATCH – Update a book by its ID, returning the book data as JSON.
DELETE – Delete a book by its ID, returning the deletion status as JSON.

## Create a folder for our code:

To begin, we will create a project for the code we are about to write.

Using the command prompt, create a folder for our code called books.

```
$ mkdir books
$ cd books
```
Create a module in which we can manage dependencies.

Run the `go mod init` command, giving it the path of the module our code will be in.

```
$ go mod init books
go: creating new go.mod: module books
```

This command creates a go.mod file in which dependencies we add will be listed for tracking.


#Code 

Using IDE/text editor, create a file called main.go in the books directory. We’ll write our Go code in this file.

In main.go, at the top of the file, add the following package declaration and main function that will be called when we run the code since a standalone program (as opposed to a library) is always in package main.

```go
package main

func main() {

}
```
To run the program use `go run .`

Beneath the package declaration, let's start writing the code for Connecting to MongoDB.

Install the qmgo using the below cmd

`go get github.com/gin-gonic/gin`

import the "github.com/qiniu/qmgo" package and declare the variable `database` and `collection` and initialize them in the main function so that it could be used later on to perform CRUD operations on Data in the Database.

```go
var database *qmgo.Database
var collection *qmgo.Collection

func main() {

	// create new Client
	const databaseURI = "mongodb://localhost:27017"
	fmt.Println("Connecting to database", databaseURI)
	ctx := context.Background()
	connection, err := qmgo.NewClient(ctx, &qmgo.Config{Uri: databaseURI})

	database = connection.Database("test")    // creating Database connection
	collection = database.Collection("books") // get the collection
	defer func() {
		if err = connection.Close(ctx); err != nil {
			fmt.Println("Closing Connection to database", databaseURI)
			panic(err)
		}
	}()

}
```


Now we proceed on to write the handlers and configuring the app to listen to a http port (in our case `8000`) in the main function.

install the gin using the below cmd

`go get github.com/gin-gonic/gin`
and import it in the main file

```go
	router := gin.Default() // create router using gin

	// register routes
	router.POST("/books", CreateBook)

	fmt.Println("Service is up & running at localhost:8000")
	router.Run(":8000") // register router to port 8000
```

Here we have registered a POST router let's go ahead and create the CreateBook function to handle the create request 

Create a new file books.go in which we will be writing the code for handling the requests

##Create the request and response structure for the books:

```go
// form:"title" to map the JSON field name to the struct
// binding:"required" to enforce the value is required
type BookCreateUpdateRequest struct {
	Title  string `form:"title" binding:"required"`
	Author string `form:"author"`
}

// json:"id" to map the struct Name to its Json field name
type BookResponse struct {
	Id        primitive.ObjectID `json:"id"`
	Title     string             `json:"title"`
	Author    string             `json:"author"`
	CreatedAt time.Time          `json:"createdAt"`
	UpdatedAt time.Time          `json:"updatedAt"`
}
```

##Create the Database model structure:

```go
type Book struct {
	field.DefaultField `bson:"inline"`
	Title              string `bson:"title" validate:"required"`
	Author             string `bson:"author"`
}
```

Let's create the CreateBookmethod to handle book creation requests.

```go
func CreateBook(ctx *gin.Context) {
	var newBook BookCreateUpdateRequest

	// to bind the received JSON to BookRequest to strip the unnecessary fields.
	if err := ctx.ShouldBind(&newBook); err != nil {
		ctx.JSON(http.StatusBadRequest, "Invalid Request")
		return
	}

	// setting data to book model struct
	book := Book{
		Title:  newBook.Title,
		Author: newBook.Author,
	}
	_, err := collection.InsertOne(ctx, &book) //Inserting the Book Data to database

	// to send error response if any error occurs
	if err != nil {
		ctx.JSON(http.StatusInternalServerError, "Something went wrong, Try again after sometime")
		return
	}

	// to send success response on completion
	ctx.JSON(http.StatusCreated, GetBooksResponse(book))
}

func GetBooksResponse(book Book) (bookResponse BookResponse) {
	// setting response for book
	bookResponse = BookResponse{
		Id:        book.DefaultField.Id,
		Title:     book.Title,
		Author:    book.Author,
		CreatedAt: book.CreateAt,
		UpdatedAt: book.UpdateAt,
	}
	return
}
```

In the above code, we use GetBooksResponse method to set the response that we are to send (we will be using this for other API responses too).

Below are the Handlers for Get, List, Update and Delete.

##Get a book detail API:

```go
func GetBook(ctx *gin.Context) {

	// to get and convert the received path variable to  desired type
	bookId, err := primitive.ObjectIDFromHex(ctx.Param("bookId"))
	if err != nil {
		ctx.JSON(http.StatusBadRequest, "Invalid Request")
		return
	}

	//Getting the Book Data from database
	var book Book
	err = collection.Find(ctx, bson.M{"_id": bookId}).One(&book)

	// to send error response if any error occurs
	if err != nil {
		ctx.JSON(http.StatusNotFound, "Book Not Found")
		return
	}

	// to send success response on completion
	ctx.JSON(http.StatusOK, GetBooksResponse(book))
}
```
In the above code the `bson.M{"_id": bookId}` is used to find the book by Id and One is used to bind the mongo Data to the book variable


###Update a book API:

```go
func UpdateBook(ctx *gin.Context) {

	// to get and convert the received path variable to  desired type
	bookId, err := primitive.ObjectIDFromHex(ctx.Param("bookId"))
	if err != nil {
		ctx.JSON(http.StatusBadRequest, "Invalid Book ID")
		return
	}

	var newBook BookCreateUpdateRequest

	// to bind the received JSON to BookRequest to strip the unnecessary fields.
	if err := ctx.ShouldBind(&newBook); err != nil {
		ctx.JSON(http.StatusBadRequest, "Invalid Request")
		return
	}

	//Getting the Book Data from database
	var book Book
	err = collection.Find(ctx, bson.M{"_id": bookId}).One(&book)

	// to send error response if any error occurs
	if err != nil {
		ctx.JSON(http.StatusNotFound, "Book Not Found")
		return
	}

	// set the updated value in the book
	book.Author = newBook.Author
	book.Title = newBook.Title

	// update in database
	err = collection.ReplaceOne(ctx, bson.M{"_id": bookId}, &book)
	if err != nil {
		ctx.JSON(http.StatusInternalServerError, "Something went wrong, Try again after sometime")
		return
	}

	// to send success response on completion
	ctx.JSON(http.StatusOK, GetBooksResponse(book))
}
```

In the above code block, `collection.ReplaceOne` is used to replace the existing document based on the condition the ReplaceOne method also updates the default field UpdateAT in the database.

##Delete Book Handler:

```go
func DeleteBook(ctx *gin.Context) {

	// to get and convert the received path variable to  desired type
	bookId, err := primitive.ObjectIDFromHex(ctx.Param("bookId"))
	if err != nil {
		ctx.JSON(http.StatusBadRequest, "Invalid Request")
		return
	}

	//Getting the Book Data from database
	var book Book
	err = collection.Find(ctx, bson.M{"_id": bookId}).One(&book)

	// to send error response if any error occurs
	if err != nil {
		ctx.JSON(http.StatusNotFound, "Book Not Found")
		return
	}

	// Deleting the book
	err = collection.RemoveId(ctx, bookId)

	// to send error response if any error occurs
	if err != nil {
		ctx.JSON(http.StatusInternalServerError, "Something went wrong, Try again after sometime")
		return
	}

	// to send success response on completion
	ctx.JSON(http.StatusOK, true)
}

```

In the above code block, `collection.RemoveId` is used to remove the specific data based on the ID provided.


##Books List Handler

```go
func GetBooks(ctx *gin.Context) {

	//Getting the Book Data to database
	var books []BookListResponse
	err := collection.Find(ctx, bson.M{}).All(&books)

	// to send error response if any error occurs
	if err != nil {
		fmt.Println(err)
		ctx.JSON(http.StatusInternalServerError, "Something went wrong, Try again after sometime")
		return
	}

	// to send success response on completion
	ctx.JSON(http.StatusOK, books)
}
```
Here in the List handler, we have used BookListResponse which is used to limit the values read from the database since the id and the name of the book would suffice in the list API. Below is the BookListResponse type.

```go
type BookListResponse struct {
	Id    primitive.ObjectID `json:"id" bson:"_id"` // bson to map mongo _id to id
	Title string             `json:"title"`
}

```
Here the `bson:"_id"` is used to map the mongo _id to the ID attribute in the response.

Now all the Handlers have been created let's register the Handlers in the router in main.go by adding the below code block after the router declaration.

```go
	router.GET("/books", GetBooks)
	router.GET("/books/:bookId", GetBook)
	router.PATCH("/books/:bookId", UpdateBook)
	router.DELETE("/books/:bookId", DeleteBook)
```

Congratulations!!! the CRUD REST API  for books in a library using Mongo and Go is successfully completed.

You will be able to find the source code in the Github repo:
{% embed https://github.com/Deeshath/books %}