Skip to main content

Command Palette

Search for a command to run...

From Zero to Go Hero: Learn Go Programming with Me - Part 4

Building CRUD API

Updated
17 min read
From Zero to Go Hero: Learn Go Programming with Me - Part 4
D

I'm a mobile/web developer 👨‍💻 who loves to build projects and share valuable tips for programmers

Follow me for Flutter, React/Next.js, and other awesome tech-related stuff 😉

Introduction

  • Heyy Gophers!!! Welcome to the Part 4 of Learning Go with Me. Thanks for coming alone to till here. I hope you are learning and enjoying this series.

  • Till now we almost covered the fundamentals of Go and now we are ready to write out the first API.

  • It’s gonna be exciting as we will learn a lot of new things which will help you in your first programming career

  • Let’s get started without wasting any more time.


  • We already have the project structure ready for us
crud-api/
│
├── cmd/
│   └── main/
│       └── main.go           # Entry point for the application
│
├── config/
│   └── config.go             # Environment variable management
│
├── internal/
│   ├── routes/
│   │   └── routes.go         # API routes definition
│   ├── books/
│   │   ├── handler.go        # HTTP handlers for book operations
│   │   ├── model.go          # Book database model and queries
│   │   └── service.go        # Business logic for books
│   ├── database/
│   │   └── db.go             # Database connection logic
│
├── .env                      # Environment variables
├── go.mod                    # Go module dependencies
└── .gitignore                # Ignored files
  • Also we have dependencies installed in our project from Part 1 of this series
go get github.com/go-chi/chi/v5
go get github.com/go-sql-driver/mysql
go get github.com/joho/godotenvfg
  • And lastly, we have the environment file ready for us
// .env
PORT=8080
DB_USER=root
DB_PASSWORD=password
DB_HOST=127.0.0.1:3306
DB_NAME=crud_api
  • Now let’s go step by step. As we already have the .env file ready for us, why not create a function to load it inside our project?

  • Before that let’s see the main.go file

// main.go

package main

func main() {
    // TODO: Load .env file

    // TODO: Setup database connection

    // TODO: Setup router

    // TODO: Run server
}
  • These are our TODOs which we need to complete

Loading .env Variables

  • To load values inside our .go files we are using godotenv package. This package helps us get environmental values inside our file.

  • We implement this function inside config.go file. So head over to config.go and let’s write LoadEnv function. But before that let’s import a few packages

// config/config.go

package config

import (
    "log"
    "os"

    "github.com/joho/godotenv"
)
  • We are using log, os and godotenv packages here.

fmt and log Packages

  • In Go we mostly use these two packages to print out values and logs in the terminal. At first, these two may look similar but they both serve different purposes.

  • log is specifically designed for logging messages. This means we can get more information like time stamping and error reporting with this. So we mostly use it during application runtime like debugging, informational logs, and error logs.

    • It includes functions like log.Fatal(Exists the program after logging) and log.Panic (Logs and then panic)
  • Whereas fmt is used for general-purpose printing text. It is commonly used for printing in a console with formatted strings. So we use it mostly when we are printing user-facing messages.

Example

fmt.Println("Hello World") // Hello World
fmt.Printf("Hello %s", "World") // Hello World

log.Fatalf("This is a fatal error.") // This is a fatal error. (After this program stops)
log.Panic("This is a panic error.") // This is a panic error. (After this program stops)

os Package

  • The os package in Go is used for tasks related to the operating system. For example accessing the files, and environment variable.

  • In our example, we are focused on accessing the .env file and it has Getenv() a function that receives the values of environment variables named by key


  • Now let’s write LoadEnv() function
import (...)

func LoadEnv() {
    err := godotenv.Load("../.env")
    if err != nil {
        log.Fatal("Error loading .env file")
    }
}
  • We get Load() function inside godotenv package. Load will read the .env file and load them into ENV for this process. Notice that you pass the path of your .env file.

  • This function returns an error so we are collecting it inside err variable and then checking if the err has anything. If the err is not nil which means something went wrong. If so, we need to show the error message and exit the program. Because if the env file is not loaded then there is no point in starting the program.

  • Now to get a specific value from the env file we need to make one function that will get us the value of the asked key from the env file

package config

import (...)

func LoadEnv() {
    ...
}

func GetEnv(key, fallback string) string {
    if value, ok := os.LookupEnv(key); ok {
        return value
    }
    return fallback
}
  • We created the GetEnv function, which takes key and fallback strings as parameters and returns a string.

  • The key is the string for which we need the value in our code. For example, inside our .env file, we have different values. If we need only DB_USER, we pass this key to the function, and it returns the related value.

  • We also accept a fallback string, so if the requested value isn’t found, we use the default fallback value.

  • Here, we are using the os package, which includes the LookupEnv function. This function takes a key as an argument and returns two things: a string and a bool. If the value is found, ok will be true; otherwise, it will be false. By checking ok, we can determine if a value was returned and decide whether to return the fallback value.

  • Let’s check if it is working or not

package main

import (
    "fmt"

    "github.com/red-star25/crud-api/config"
)

func main() {
    config.LoadEnv()
    fmt.Println(config.GetEnvValue("DB_USER", "root"))

       // TODO: Setup database connection

    // TODO: Setup router

    // TODO: Run server
}

Output:

root
  • That’s awesome. Let’s now set out the Database Connection

Setting Up the Database Connection

Importing Packages

  • As you know we are using MySQL for storing our application’s data. We need to first set up the connection and create an instance of the SQL DB.

  • Head over to db.go file and let’s first import all the required packages

package database

import (
    "database/sql"
    "fmt"
    "log"
    "os"

    _ "github.com/go-sql-driver/mysql"
)
  • We have seen the usage of fmt, log and os. Let’s see what are the remaining ones.

  • database/sql: This is a built-in library for SQL database interactions.

  • _ "github.com/go-sql-driver/mysql": Imports the MySQL driver as a side effect to register it with database/sql. Here _ (underscore) represent that this import is used as a side effect. Which means this package is not directly used.

Creating Database

var DB *sql.DB
  • Here type DB is a pointer to sql.DB an object. We are making it globally accessible so that anyone can reuse it without creating it again and again on each query

  • You must be wondering Why it is a type of pointer.

    • So, If DB were a simple variable (not a pointer), any changes made to it inside a function, wouldn't affect the global variable itself. Instead, Go would create a copy of the value, and the global DB would remain unchanged.

    • By using a pointer, it updates the global DB directly. This makes the database connection immediately available to all parts of the application.

Connect function

  • Now let’s create a function Connect that will be responsible for opening the database connection and making a connection.
func Connect() {
    dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s",
        config.GetEnvValue("DB_USER", "root"),
        config.GetEnvValue("DB_PASSWORD", ""),
        config.GetEnvValue("DB_HOST", "localhost"),
        config.GetEnvValue("DB_NAME", "crud_api"),
    )
}
  • Here we have created a variable dsn(Data Source Name) which is a type of string. So before we connect to MySQL we need to have a URL to which we want to connect.

  • So here we have used a formatted string from fmt package. Here function Sprintf returns a formatted string without printing it to the console. %s is a placeholder for string values here.

  • And we are getting all the values from our .env file using config’s GetEnvValue function.

  • Example dsn

username:password@tcp(localhost)/mydatabase

Opening Connection

  • Now that our data source URL is ready, we are ready to open our SQL connection.

  • To do that mysql provides Open() function which takes

    • driverName (which is nothing but a database name, ex: MySQL, MongoDB, Postgres, etc). You can provide any driver here and open the connection and

    • dataSourceName is a URL of the database that we just created above.

var err error
DB, err = sql.Open("mysql", dsn)
if err != nil {
    log.Fatalf("Error connecting to database: %v", err)
}
  • This function returns two things: db object and err.

  • So what we are doing here is, we are assigning the returned SQL db object to our globally defined DB.

  • And then we check if there is any error occurred or not.

You will see this type of error handling thoughtout any Go project. This is a convention in Go. It might seems little overdoing but it is a good practice that we are handling error just after calling function.

Also note that there is no try catch or finally keywords in Go. You can read why they are using this type of error handling in this article : https://go.dev/doc/faq#exceptions

Verifying the DB Connection

  • To verify whether the connection is successful or not we have Ping function that verifies if the database connection is still alive, and it also establishes the connection if necessary.
if err := DB.Ping(); err != nil {
    log.Fatalf("Error verifying database connection: %v", err)
}
  • That’s pretty much it. We can log the message after this that the Database is connected successfully.

Final db.go code

package database

import (
    "database/sql"
    "fmt"
    "log"

    _ "github.com/go-sql-driver/mysql"
    "github.com/red-star25/crud-api/config"
)

var DB *sql.DB

func Connect() {
    dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s",
        config.GetEnvValue("DB_USER", "root"),
        config.GetEnvValue("DB_PASSWORD", ""),
        config.GetEnvValue("DB_HOST", "localhost"),
        config.GetEnvValue("DB_NAME", "crud_api"),
    )

    var err error
    DB, err = sql.Open("mysql", dsn)
    if err != nil {
        log.Fatalf("Error connecting to database: %v", err)
    }

    if err := DB.Ping(); err != nil {
        log.Fatalf("Error verifying database connection: %v", err)
    }

    log.Println("Database connected successfully!")

}
  • And now call this Connect function inside main.go
package main

import (
    "github.com/red-star25/crud-api/config"
    "github.com/red-star25/crud-api/database"
)

func main() {
    config.LoadEnv()

    database.Connect()

    // TODO: Setup router

    // TODO: Run server

}
  • If everything goes right then you will see this message after you run the program.

    Make sure you have MySQL up and running on your computer.


Creating Book model

  • We are creating CRUD API for managing Books. So in order to view data in a structured manner with all the required fields we need to create a Book model using struct

In Go, the term "model" refers to a structured representation of data used within an application, typically to map data between a program and an external data source, such as a database or an API. Models are often implemented as structs in Go, which are a way to define and organize related data fields.

  • Let’s create our Book model
// internal/books/model.go
package books

type Book struct {
    ID     int
    Title  string
    Author string
    Year   int
}
  • Here we have four fields inside our Book struct. ID, Title, Author, Year.

  • That’s easy, right? Yes. But there is one more thing which we need to do. So as we know that when we deal with APIs, we use JSON to serialize and deserialize the data that we are getting from the user and data we are sending to the user.

  • And generally, they are represented in lowercase like id, name, author, year like that. But if you see our Book struct we have the first letter capital for each field. And that is intentional. Because we need those struct’s fields outside to use. And to make it globally accessible we capitalize the first letter of any variable.

  • So to resolve this issue, we use what we call in go “ struct tags “. In other programming languages, we refer to this as “ annotations “.

  • This helps us convert these struct fields to proper naming conventions. Let’s see how we do that

package books

type Book struct {
    ID     int    `json:"id"`
    Title  string `json:"title"`
    Author string `json:"author"`
    Year   int    `json:"year"`
}
  • So what we do is after giving the type of the struct field we use backticks ` `. And in there we write json: and then whatever name we want.

Implement Database Queries (CRUD)

  • Now that our database is up and running let’s write database queries for Create, Read, Update, and Delete.

  • First of all, let’s import two required packages

package books

import (
    "crud-api/internal/database"
    "errors"
)
  • We are importing a database for accessing the DB object. Using this we can perform all the queries.

  • And errors we use for handling errors.

The GetAllBooks Function

  • Firstly we are creating a function to get all the books from the database
func GetAllBooks() ([]Book, error) {
    rows, err := database.DB.Query("SELECT * FROM books")
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    ...
}
  • Here we created a function called GetAllBooks which is responsible for getting all the books from the database.

  • It returns two values:

    • [ ]Book: A slice of struct Book.

    • error: Go’s error object.

  • Inside the function body, we are making a query to the SQL database using the DB variable’s Query function. It takes a string where we write a raw SQL query.

  • In this function, we need all the books with data. And to do that we wrote this query “SELECT * FROM books“. Which will get all the books from the database.

  • And then we checked if it threw any errors

  • After that, we defer call the rows.Close() to close it once this function is finished.


  • This rows object allows us to iterate over the results we get from the Query. And to iterate over the results we have rows.Next() function which goes over all the results.

  • We can take advantage of this function and create a for loop inside which we can fill all the data inside a slice of the book

func GetAllBooks() ([]Book, error) {    
    ...
    var books []Book
    for rows.Next() {
        var book Book
        if err := rows.Scan(&book.ID, &book.Title, &book.Author, &book.Year); err != nil {
            return nil, err
        }
        books = append(books, book)
    }
    return books, nil
}
  • Here we have created a temporary slice of the book variable. In this, we will fill in all the data.

  • Then we looped through rows.Next() which will go over all the rows and inside the for the body we are getting all mapping the data in a row to the Book struct using rows.Scan

  • And after that, we are appending that book to the books slice

  • And once every row is scanned we return it from the function.

NOTE: Make sure you have the book table in MySQL database: If you don’t have one then create it and then run the project, otherwise it will throw an error

You can run this query inside your MySQL Workbench

CREATE TABLE books (
    id INT AUTO_INCREMENT PRIMARY KEY,
    title VARCHAR(255) NOT NULL,
    author VARCHAR(255) NOT NULL,
    year INT NOT NULL
);
  • Let’s test GetAllBooks.
// main.go

func main() {
    config.LoadEnv()

    database.Connect()

    data, err := books.GetAllBooks()
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("Books:",data)
    // TODO: Setup router

    // TODO: Run server

}

Output:

  • It ran!!!!

a man in a suit and black turtleneck is giving a yes sign


  • This is great! Now let’s create a Handler function for this function. Because we need to send this data to the user too, right? Till now we just got the data from the database.

  • To do that head over to handler.go file


Setting up HTTP Handlers

  • Let’s first import the required packages
// handler.go

import (
    "encoding/json"
    "net/http"
)
  • Here encoding/json is used. This is used for converting JSON data into Go structures and Go data structures into JSON.

  • net/http is used for handling HTTP requests.

  • I’ll first show all the code to you and then we will understand what it does

package books

import (
    "encoding/json"
    "net/http"
)

func GetAllBooksHandler(w http.ResponseWriter, r *http.Request) {
    books, err := GetAllBooks()
    if err != nil {
        http.Error(w, "Failed to fetch books", http.StatusInternalServerError)
        return
    }

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(books)
}
  • func GetAllBooksHandler(w http.ResponseWriter, r *http.Request) {

    • Here we have two parameters:

      • w http.ResponseWriter: This is used for sending data back to the client.

      • r *http.Request: This is used for getting data from the client.

Fetching the Books

  • books, err := GetAllBooks()

    • Here, the function GetAllBooks is called to retrieve a list of books. As we saw, that function will get the data from the database and return all the books with an err object.

Error Handling

if err != nil {
    http.Error(w, "Failed to fetch books", http.StatusInternalServerError)
    return
}
  • If that function returns any error, we check it inside this if block and send back the HTTP error of InternalServerError.

  • And then exiting the function with return

  • Here, The http.Error function simplifies sending error responses. This has all the different types of errors you can throw according to your functionalities.

Setting the Response Header

w.Header().Set("Content-Type", "application/json")
  • This line sets the Content-Type header of the response to application/json. It informs the client that the response body contains JSON data.

Encoding and Sending the Response

json.NewEncoder(w).Encode(books)
  • Now we need to encode the slice of a book in JSON format for the client. To do data we use json Encoder method.

  • This sent the data as the HTTP response. And it ensures compatibility with clients expecting data in JSON format.


Configuring Routes

  • Now, how these handlers are going to trigger? Of course when the user hits a certain URL, or to be specific an Endpoint from their end.

  • And so we want to return data for that particular endpoint/route right? For that, we need to create a route called /book.

  • And to create routes we are using the Chi package.

  • Go to routes.go and paste the below code

package routes

import (
    "crud-api/internal/books"

    "github.com/go-chi/chi/v5"
)

func SetupRouter() *chi.Mux {
    r := chi.NewRouter()

    r.Get("/books", books.GetAllBooksHandler)

    return r
}
  • Here the SetupRouter function will configure all the routes.

  • First, we need to create an instance of chi router. This router will handle the incoming HTTP requests and route them to the appropriate handlers.

  • r := chi.NewRouter() - This line does that

  • And now using this variable r we can create a map of the routes to the handlers.

r.Get("/books", books.GetAllBooksHandler)
  • For different HTTP request, we have different HTTP Methods in r. POST, UPDATE, DELETE,etc

  • Here we need GET as we are getting the data from the database. Inside r.Get(), first, we need to give the route as /books. This means once this endpoint is hit by the client, the handler defined in the second parameter will be triggered.

  • And at the end, we are returning the pointer to the chi.Mux because we need it for the server in the main.go. You will see why below.


Finalizing the main.go file

  • Now that we have everything set up we can start the server and listen to the endpoints.
package main

import (
    "log"
    "net/http"

    "github.com/red-star25/crud-api/config"
    "github.com/red-star25/crud-api/database"
    "github.com/red-star25/crud-api/internal/routes"
)

func main() {
    config.LoadEnv()
    database.Connect()

    router := routes.SetupRouter()
    port := config.GetEnvValue("PORT", "8080")
    log.Printf("Server running on port %s", port)
    log.Fatal(http.ListenAndServe(":"+port, router))
}
  • Inside the main function, we are loading the environment variables.

  • Then we connect to the database.

  • And then we call SetupRouter to configure all the routes.

  • To run the server we have ListenAndServe function inside http package. Inside we have to first pass the address and then the router that we set up. And we also have to check for any errors. So we are wrapping this function inside log.Fatal


Running The Server

  • Now let’s cross our fingers and check if everything is working. To check the functionality of our program. We are using the Postman application.

  • So open it up and run it

a baby is sitting in a crowd with a woman holding him and screaming yes .

  • You will see an empty slice because we have not added any data yet. We will be implementing other HTTP methods in the next article.

Wrapping Up

  • If you came this far to the end of this article, then congratulations for making it.

  • In the next article, we will be implementing the remaining CRUD operations which are Update, Delete, and Create. It will be fun because now we have the base setup for us, so now we just need to work on the functionalities.

  • Hope you learned something from this article. There are lots of blogs coming in the future with more amazing topics and projects and I am so excited to write about all of it.

  • See you in the next blog, until then….