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:
- Design API endpoints.
- Create a folder for code.
- Connect to MongoDB using qmgo
- 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.
- An installation of MongoDB 6.0 or later. For installation instructions, visit
- 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.
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.
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
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:
// 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:
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.
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:
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:
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:
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
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.
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.
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:
Top comments (0)