With the release of Go 1.22, the net/http package now provides all you need for advanced routing in Golang when building Web APIs. However, understanding how to utilize the net/http package can be quite challenging, especially for advanced features like middleware, sub-routing, path parameters, HTTP methods, etc.
Therefore, in this article, we will build a basic CRUD API to cover almost 80% of what you need to know when building an API with the net/http package.
Here is the roadmap we will follow: First, we will set up a SQLite database with GORM. Then, we’ll create route handlers to process the HTTP requests and establish routes for these handlers.
Once completed, you can spin up a frontend app we created in a previous article to interact with the Go API. Don’t worry if you didn’t complete the article related to the frontend app; I will provide the GitHub link so you can clone and run it on your machine.
More practice:
- Golang CRUD API Example with GORM and MySQL
- Golang CRUD RESTful API with SQLC and PostgreSQL
- Create CRUD API in Golang using Fiber and GORM
- Build a Simple API with Rust and Rocket
- Build a CRUD API with Rust and MongoDB
- Build a Simple API with Rust and Actix Web
- Build a CRUD API with Node.js and Sequelize
Run the Go Project on Your Computer
To get the Go API project up and running on your machine, follow these simple steps:
- Download or clone the Golang project from its GitHub repository at https://github.com/wpcodevo/go-standard-lib-crud-app. Once you have the project, open it in your preferred IDE or text editor.
- Next, go to your terminal and execute the
go run main.go
command. This will install all the necessary packages, migrate the GORM schema to the SQLite database, and start the HTTP server. - To test the API endpoints, you can use Postman to make HTTP requests to the Go server. First, import the
Note App.postman_collection.json
file available in the root directory, which contains predefined HTTP requests to make your life easier. - Alternatively, you can follow the instructions below if you prefer to interact with the API using a front-end application.
Run the API with a Frontend App
If you’re looking for a more detailed guide on how to build the React.js CRUD app, you can check out the post titled “Build a React.js CRUD App using a RESTful API“. However, if you want to just run the frontend with the Go API without having to write any code, follow the steps below:
- First, ensure that you have Node.js and PNPM installed on your machine. If not, you can download and install them from the official websites.
- Next, obtain the source code for the React CRUD project by either cloning the repository or downloading it from https://github.com/wpcodevo/reactjs-crud-note-app, and then open it in your preferred IDE or text editor.
- In the console, navigate to the root directory of the project and execute the command
pnpm install
to install all of the necessary dependencies. - Once the dependencies have been installed, run the command
pnpm dev
to start the Vite development server. - To test the CRUD functionalities, open the app in your browser by navigating to
http://localhost:3000/
. Keep in mind that accessing the React app at http://127.0.0.1:3000 might result in a “site can’t be reached” error or CORS errors. Therefore, it’s best to usehttp://localhost:3000/
.
Set up the Golang Project
As always, we need to start by generating a new Go project. To do this, navigate to the directory where you want to store the project’s source code and run the commands below. These commands will create a new directory and initialize it as a Golang project. Remember to replace the <username>
placeholder with your GitHub username.
mkdir go-standard-lib-crud-app
go mod init github.com/wpcodevo/go-standard-lib-crud-app
Next, we need to install a couple of dependencies or packages we will need while building the API. Run the commands below to install them:
go get github.com/go-playground/validator/v10
go get gorm.io/driver/sqlite
go get -u gorm.io/gorm
go get github.com/google/uuid
go get github.com/rs/cors
validator
– This package allows you to validate struct fields based on tags added to the struct fields.sqlite
– This package provides SQLite driver for the GORM ORM library in Go.gorm
– An ORM library for handling database operations in Go.uuid
– This package allows you to generate and manage UUIDs in Go.cors
– A package for handling CORS-related headers and requests in Go.
Set up GORM for Database Operations
With the dependencies installed, let’s start working on the database-related tasks, which include creating the database schemas, handling the validation logic, and connecting the Go project to the SQLite server.
Create the Database Schema
Let’s start by creating the GORM model called Note, which we will later use to generate its corresponding table and columns in the database using GORM. To do this, create a new file named model.go
in the root directory and include the following code:
model.go
package main
import (
"time"
"github.com/go-playground/validator/v10"
"github.com/google/uuid"
"gorm.io/gorm"
)
type Note struct {
ID string `gorm:"type:char(36);primary_key" json:"id,omitempty"`
Title string `gorm:"type:varchar(255);uniqueIndex:idx_notes_title,LENGTH(255);not null" json:"title,omitempty"`
Content string `gorm:"not null" json:"content,omitempty"`
Category string `gorm:"varchar(100)" json:"category,omitempty"`
Published bool `gorm:"default:false;not null" json:"published"`
CreatedAt time.Time `gorm:"not null;default:'1970-01-01 00:00:01'" json:"createdAt,omitempty"`
UpdatedAt time.Time `gorm:"not null;default:'1970-01-01 00:00:01';ON UPDATE CURRENT_TIMESTAMP" json:"updatedAt,omitempty"`
}
func (note *Note) BeforeCreate(tx *gorm.DB) (err error) {
note.ID = uuid.New().String()
return nil
}
var validate = validator.New()
type ErrorResponse struct {
Field string `json:"field"`
Tag string `json:"tag"`
Value string `json:"value,omitempty"`
}
func ValidateStruct[T any](payload T) []*ErrorResponse {
var errors []*ErrorResponse
err := validate.Struct(payload)
if err != nil {
for _, err := range err.(validator.ValidationErrors) {
var element ErrorResponse
element.Field = err.StructNamespace()
element.Tag = err.Tag()
element.Value = err.Param()
errors = append(errors, &element)
}
}
return errors
}
type CreateNoteSchema struct {
Title string `json:"title" validate:"required"`
Content string `json:"content" validate:"required"`
Category string `json:"category,omitempty"`
Published bool `json:"published,omitempty"`
}
type UpdateNoteSchema struct {
Title string `json:"title,omitempty"`
Content string `json:"content,omitempty"`
Category string `json:"category,omitempty"`
Published *bool `json:"published,omitempty"`
}
In the above code, apart from the GORM model, we also have a function called ValidateStruct
, which we will use alongside the CreateNoteSchema
and UpdateNoteSchema
structs to validate the incoming requests for the CREATE and UPDATE operations.
Connect to the Database Server
Now that we’ve defined the GORM model, let’s create a utility function to establish a connection pool with the SQLite database and automatically migrate the GORM model to it. To do this, create a file named initializer.go
in the root directory and add the following code.
initializer.go
package main
import (
"log"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
var DB *gorm.DB
func ConnectDB() error {
var err error
DB, err = gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{})
if err != nil {
return err
}
DB.Logger = logger.Default.LogMode(logger.Info)
log.Println("Running Migrations")
err = DB.AutoMigrate(&Note{})
if err != nil {
return err
}
log.Println("🚀 Connected Successfully to the Database")
return nil
}
Later, we’ll invoke the ConnectDB
function to create the connection pool and apply the migrations, but for now, let’s focus on the CRUD operations in the next section.
Perform CRUD Operations
With the database-related code now completed, let’s start implementing the CRUD operations. To do this, we will create route handlers, also known as controllers, that will use the CRUD functions provided by the GORM library to perform operations against the database. Start by creating a new file named handlers.go
in the root directory and include the following code imports and package definition:
package main
import (
"encoding/json"
"net/http"
"strconv"
"strings"
"time"
"gorm.io/gorm"
)
Perform CREATE Operation
Let’s start with the CREATE operation. Here, we will create a function called CreateNoteHandler
. Within it, we will validate the incoming request using the CreateNoteSchema
struct along with the validator package to ensure that the user includes the right data in the request body.
Next, we will store the data in the database and return a copy in JSON format to the user. To do this, add the following code to the handlers.go
file.
handlers.go
func CreateNoteHandler(w http.ResponseWriter, r *http.Request) {
var payload CreateNoteSchema
// Decode JSON request body into the payload struct
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "fail",
"message": err.Error(),
})
return
}
// Validate payload struct
errors := ValidateStruct(&payload)
if errors != nil {
w.WriteHeader(http.StatusBadRequest)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(errors)
return
}
now := time.Now()
newNote := Note{
Title: payload.Title,
Content: payload.Content,
Category: payload.Category,
Published: payload.Published,
CreatedAt: now,
UpdatedAt: now,
}
// Save new note to the database
result := DB.Create(&newNote)
if result.Error != nil {
if strings.Contains(result.Error.Error(), "UNIQUE constraint failed") {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusConflict)
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "fail",
"message": "Title already exists, please use another title",
})
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadGateway)
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "error",
"message": result.Error.Error(),
})
return
}
// Return success response
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "success",
"data": map[string]interface{}{
"note": newNote,
},
})
}
Perform READ Operation
Let’s proceed to handle the READ operation of CRUD. We have two READ operations to handle: one for retrieving a paginated list of items and the other for retrieving an item by a specific ID.
To begin, let’s address the first READ operation, which involves fetching multiple items. Add the following code to the handlers.go
file.
handlers.go
func FindNotes(w http.ResponseWriter, r *http.Request) {
page := r.URL.Query().Get("page")
limit := r.URL.Query().Get("limit")
if page == "" {
page = "1"
}
if limit == "" {
limit = "10"
}
intPage, err := strconv.Atoi(page)
if err != nil {
http.Error(w, "Invalid page parameter", http.StatusBadRequest)
return
}
intLimit, err := strconv.Atoi(limit)
if err != nil {
http.Error(w, "Invalid limit parameter", http.StatusBadRequest)
return
}
offset := (intPage - 1) * intLimit
var notes []Note
results := DB.Limit(intLimit).Offset(offset).Find(¬es)
if results.Error != nil {
http.Error(w, results.Error.Error(), http.StatusBadGateway)
return
}
// Return success response
response := map[string]interface{}{
"status": "success",
"results": len(notes),
"notes": notes,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
In the above code snippet, we first retrieve the page and limit query parameters from the request URL. Then, we use their values to calculate the offset and limit for paginating the results returned by the database.
Now, let’s proceed to retrieve an item by its ID. Navigate to the handlers.go
file and include the code below:
handlers.go
func FindNoteById(w http.ResponseWriter, r *http.Request) {
noteID := r.PathValue("noteId")
var note Note
result := DB.First(¬e, "id = ?", noteID)
if err := result.Error; err != nil {
if err == gorm.ErrRecordNotFound {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusNotFound)
response := map[string]interface{}{
"status": "fail",
"message": "No note with that ID exists",
}
json.NewEncoder(w).Encode(response)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadGateway)
response := map[string]interface{}{
"status": "fail",
"message": err.Error(),
}
json.NewEncoder(w).Encode(response)
return
}
response := map[string]interface{}{
"status": "success",
"data": map[string]interface{}{
"note": note,
},
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
Within the FindNoteById
route handler, we first extract the ID of the record to be retrieved from the request URL. Then, we proceed to make a request to the database to fetch the item by the provided ID. If a record is found, we return it in JSON format. Otherwise, a 404 Not Found error will be returned to the user.
Perform UPDATE Operation
Moving on, let’s now handle the UPDATE operation. This one is quite challenging since we need a way to partially update the columns in the database, which we can represent with a PATCH HTTP method.
This means we only update the fields provided in the request body in the database instead of updating the entire record. So, still inside the handlers.go
file, add the following code to it.
handlers.go
func UpdateNote(w http.ResponseWriter, r *http.Request) {
noteID := r.PathValue("noteId")
var payload UpdateNoteSchema
err := json.NewDecoder(r.Body).Decode(&payload)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
var note Note
result := DB.First(¬e, "id = ?", noteID)
if err := result.Error; err != nil {
if err == gorm.ErrRecordNotFound {
errorResponse := map[string]interface{}{
"status": "fail",
"message": "No note with that ID exists",
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusNotFound)
json.NewEncoder(w).Encode(errorResponse)
return
}
http.Error(w, err.Error(), http.StatusBadGateway)
return
}
updates := make(map[string]interface{})
if payload.Title != "" {
updates["title"] = payload.Title
}
if payload.Category != "" {
updates["category"] = payload.Category
}
if payload.Content != "" {
updates["content"] = payload.Content
}
if payload.Published != nil {
updates["published"] = payload.Published
}
updates["updated_at"] = time.Now()
DB.Model(¬e).Updates(updates)
response := map[string]interface{}{
"status": "success",
"data": map[string]interface{}{
"note": note,
},
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
Quite a lot is happening in the above code. Here’s a breakdown of the UpdateNote
function:
- First, it extracts the note ID from the request URL.
- Then, it decodes the JSON request body into an
UpdateNoteSchema
struct to get the updated note data. - Next, it queries the database to find the note with the given ID.
- If the note is not found, it returns a 404 Not Found response.
- If there’s any other error during the database operation, it returns a 502 Bad Gateway response.
- Next, it constructs a map of updates based on the fields provided in the request body.
- Then, it updates the note in the database with the provided data.
- Finally, it constructs a success response containing the updated note data and sends it back to the client as JSON.
Perform DELETE Operation
Let’s now handle the DELETE operation of CRUD. This implementation is quite simple, as we need to find the record by its ID and delete it from the database. To do this, open the handlers.go
file and include the following code:
handlers.go
func DeleteNote(w http.ResponseWriter, r *http.Request) {
noteID := r.PathValue("noteId")
result := DB.Delete(&Note{}, "id = ?", noteID)
if result.RowsAffected == 0 {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusNotFound)
response := map[string]interface{}{
"status": "fail",
"message": "No note with that ID exists",
}
json.NewEncoder(w).Encode(response)
return
} else if result.Error != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadGateway)
response := map[string]interface{}{
"status": "error",
"message": result.Error.Error(),
}
json.NewEncoder(w).Encode(response)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
response := map[string]interface{}{
"status": "success",
"message": "Note deleted successfully",
}
json.NewEncoder(w).Encode(response)
}
Create Routes for HTTP Handlers
Finally, let’s wire everything together in the main file by creating routes for the HTTP route handlers, connecting to the database, setting up CORS, and starting the HTTP server. Create a file named main.go
in the root directory and include the following code:
main.go
package main
import (
"encoding/json"
"log"
"net/http"
"time"
"github.com/rs/cors"
)
func init() {
// Initialize database
err := ConnectDB()
if err != nil {
log.Fatalf("Failed to connect to the database: %v", err)
}
}
func main() {
// Create Router
router := http.NewServeMux()
router.HandleFunc("GET /api/healthchecker", HealthCheckHandler)
router.HandleFunc("PATCH /api/notes/{noteId}", UpdateNote)
router.HandleFunc("GET /api/notes/{noteId}", FindNoteById)
router.HandleFunc("DELETE /api/notes/{noteId}", DeleteNote)
router.HandleFunc("POST /api/notes/", CreateNoteHandler)
router.HandleFunc("GET /api/notes/", FindNotes)
// Custom CORS configuration
corsConfig := cors.New(cors.Options{
AllowedHeaders: []string{"Origin", "Authorization", "Accept", "Content-Type"},
AllowedOrigins: []string{"http://localhost:3000"},
AllowedMethods: []string{"GET", "POST", "PATCH", "DELETE"},
AllowCredentials: true,
})
// Wrap the router with the logRequests middleware
loggedRouter := logRequests(router)
// Create a new CORS handler
corsHandler := corsConfig.Handler(loggedRouter)
server := http.Server{
Addr: ":8000",
Handler: corsHandler,
}
log.Println("Starting server on port :8000")
if err := server.ListenAndServe(); err != nil {
log.Fatalf("Server error: %v", err)
}
}
type wrappedWriter struct {
http.ResponseWriter
statusCode int
}
func (w *wrappedWriter) WriteHeader(statusCode int) {
w.ResponseWriter.WriteHeader(statusCode)
w.statusCode = statusCode
}
func logRequests(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
wrapped := &wrappedWriter{
ResponseWriter: w,
statusCode: http.StatusOK,
}
next.ServeHTTP(wrapped, r)
elapsed := time.Since(start)
log.Printf("Received request: %d %s %s %s", wrapped.statusCode, r.Method, r.URL.Path, elapsed)
})
}
func HealthCheckHandler(w http.ResponseWriter, r *http.Request) {
response := map[string]string{
"status": "success",
"message": "Welcome to Go standard library",
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
Conclusion
And that brings us to the end. In this article, you learned how to create a CRUD API using the net/http package in Go without using any third-party routing library. I hope you found this article helpful and enjoyable. If you have any questions or feedback, feel free to leave them in the comment section below.