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

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
.gofiles we are usinggodotenvpackage. This package helps us get environmental values inside our file.We implement this function inside
config.gofile. So head over toconfig.goand let’s writeLoadEnvfunction. 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,osandgodotenvpackages 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.
logis 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) andlog.Panic(Logs and then panic)
- It includes functions like
Whereas
fmtis 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)
More on
fmtvisit: https://pkg.go.dev/fmtMore on
logvisit: https://pkg.go.dev/log
os Package
The
ospackage 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 bykey
- 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 insidegodotenvpackage.Loadwill read the.envfile 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
errvariable and then checking if theerrhas anything. If theerris notnilwhich 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
GetEnvfunction, which takeskeyandfallbackstrings as parameters and returns a string.The
keyis the string for which we need the value in our code. For example, inside our.envfile, we have different values. If we need onlyDB_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
ospackage, which includes theLookupEnvfunction. This function takes akeyas an argument and returns two things: astringand abool. If the value is found,okwill be true; otherwise, it will be false. By checkingok, 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.gofile 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,logandos. 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 withdatabase/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.DBan object. We are making it globally accessible so that anyone can reuse it without creating it again and again on each queryYou must be wondering Why it is a type of pointer.
So, If
DBwere 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 globalDBwould remain unchanged.By using a pointer, it updates the global
DBdirectly. This makes the database connection immediately available to all parts of the application.
Connect function
- Now let’s create a function
Connectthat 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
fmtpackage. Here functionSprintfreturns a formatted string without printing it to the console.%sis a placeholder forstringvalues here.And we are getting all the values from our
.envfile using config’sGetEnvValuefunction.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
mysqlprovidesOpen()function which takesdriverName(which is nothing but a database name, ex: MySQL, MongoDB, Postgres, etc). You can provide any driver here and open the connection anddataSourceNameis 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:
dbobject anderr.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
trycatchorfinallykeywords 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
Pingfunction 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
Connectfunction insidemain.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
structsin 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,yearlike 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
errorswe 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
GetAllBookswhich 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
Queryfunction. 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
rowsobject allows us to iterate over the results we get from the Query. And to iterate over the results we haverows.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 usingrows.ScanAnd after that, we are appending that book to the
bookssliceAnd 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!!!!

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.gofile
Setting up HTTP Handlers
- Let’s first import the required packages
// handler.go
import (
"encoding/json"
"net/http"
)
Here
encoding/jsonis used. This is used for converting JSON data into Go structures and Go data structures into JSON.net/httpis 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
GetAllBooksis 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.
- Here, the function
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
returnHere, The
http.Errorfunction 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-Typeheader of the response toapplication/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.goand 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 thatAnd now using this variable
rwe 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,etcHere 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
ListenAndServefunction insidehttppackage. 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 insidelog.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


- 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….






