Learning By Doing Golang - ToDo App Part 3.1 - Application Layer (Core Logic)

NaveenKumar VRNaveenKumar VR
11 min read

Now that we've set up our database and created the schema for our ToDo app, it's time to focus on the core logic — what we call the Application Layer of the ToDo app.

If you're just joining the journey, you can follow the entire series from here:

🧠 Setting the Context: The Core Logic of Our App

The core functionality of our ToDo application includes:

  • ✅ Creating a ToDo item

  • ✏️ Updating a ToDo item

  • ❌ Deleting a ToDo item

  • 📄 Reading (fetching) ToDo items

In this section, we’ll implement the application logic that powers these features.

We'll be writing code that defines what the app should do when a user makes a specific request — and what kind of response the app should return.

🛠️ How Are We Going to Do This?

Just like in the previous blogs of this series, we’ll follow a two-step approach:

  1. Start with the basic (native) approach to understand how things work at the core level.

  2. Evaluate and adopt a better alternative library that simplifies and enhances the process.

  3. Use the selected method to implement the entire logic and test it end-to-end.

Let’s get started and build the Application Layer of our ToDo app! 🚀

🌐 Understanding HTTP in Our Web Application

Since we’re building a web application, all user interactions will happen through HTTP requests. We should handle this request to serve the user.

💡 Wait… what do you mean by handling HTTP requests?

Great question! In a web app, users interact with the backend using HTTP — the protocol behind the web. For example, if a user wants to create a ToDo item, they would make a request like:

curl -X POST http://mytodoapp/create \
  -H "Content-Type: application/json" \
  -d '{
    "task_name": "Write blog post",
    "description": "Complete the blog post for the Golang ToDo app",
    "status": "NotStarted",
    "end_date": "2024-06-30T23:59:00Z"
  }'

You might be wondering: “What exactly is that?” Don’t worry — we’ll cover it in more detail soon. For now, just understand that users will interact with our app using HTTP URLs and endpoints (like /create, /update, etc.) to trigger certain actions. And when we say "handling HTTP requests", we mean:

The application should be able to receive the request, extract any information from it (like request body or URL parameters), and perform the appropriate action in response.

How Do We Handle HTTP in Golang? To handle HTTP in Go, there are several libraries available. One of the most basic and native options is: net/http

🛠 Let’s Start with Creating a ToDo Item

To begin, we’ll focus on one core operation: creating a new ToDo item.

We’ll implement an HTTP handler using the net/http package that listens for a POST request, extracts data from the request body, and passes it to our application logic (which we already connected to the database layer).

In the next section, we’ll walk through how to:

  1. Define an HTTP handler function

  2. Read and parse the JSON body from the client

  3. Validate and use the data to create a new ToDo

  4. Return a meaningful response back to the user

Let’s dive in and build the POST /todo/create endpoint using net/http.

🚦 Recollecting what we will be doing

From the previous step, we now understand that:

  • ✅ We need to import an HTTP handler to allow our Go application to serve web requests

  • ✅ To read and parse the incoming request body (usually in JSON), we need a package for decoding JSON. A quick Google search shows that Go provides a standard library for this:"encoding/json"

  • ✅ We need to write the logic to:

    1. Receive incoming HTTP requests

    2. Read the data (like a new ToDo item) from the request body

    3. Pass it to our business logic (e.g., CreateToDo)

    4. Return a proper response to the user

▶️ Let’s Code from Where We Left

If you're following along, continue from the code snippet in the last blog. We'll now build the ToDoHanlderCreate using the net/http and encoding/json packages.

Let’s plug that into our main.go file from our last blog and get started 🚀

package main

import (
    "fmt"
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
)

var (
    // Declare a global variable to hold the DB connection
    db *gorm.DB // Type: pointer to gorm.DB
)

type ToDo struct{
    gorm.Model
    TaskName    string     `gorm:"not null" json:"title"`
    Description string     `gorm:"type text" json:"description"`
    Status      string     `gorm:"default:NotStarted" json:"status" validate:"required,oneof=NotStarted InProgress Pending Completed"`
    EndDate     *time.Time `json:"end_date"`
}

// ConnectDB establishes the connection to the MySQL database
func ConnectDB() {
    // Data Source Name (DSN): contains database connection info
    dsn := "root:password@tcp(127.0.0.1:3306)/todo?charset=utf8&parseTime=True&loc=Local"

    // Connect to the DB using GORM and MySQL driver
    db_conn, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
    if err != nil {
        // If connection fails, panic and log the error
        panic("Failed to connect to the database: " + err.Error())
    }

    /*Why I'm passing value here why can't I pass it like db,err. 
    If I do that I have to either add err variable globally or remove db global var as I'm using := so I used a var to exchnage value
    */
    db = db_conn 
}

func InitDB(){
    ConnectDB() // This will create new connection and the value is stored in DB variable

    // Here we are saying that Use the Schema &ToDo and create table using ou DB connection 'db'
    //'{}' defines that create empty table with that ToDo Struct Schema
    db.AutoMigrate(&ToDo{})
}

// CreateToDo inserts a new ToDo item into the database.
// It takes a pointer to a ToDo struct as input and returns the same pointer after insertion.
func CreateToDo(todo *ToDo) *ToDo {
    // Use GORM's Create method to insert the new record into the database.
    // The &todo tells GORM to insert the data from the memory location of the passed ToDo.
    db.Create(&todo)
    // Return the same pointer, now updated with DB-generated fields (like ID, CreatedAt, etc.)
    return todo
}

// .... Truncating other code you can refer from the link provided above
...
...
...

Let’s import and modify the code

package main

import (
    "fmt"
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
    "time"
    "net/http"
    "encoding/json"
)

var (
    // Declare a global variable to hold the DB connection
    db *gorm.DB // Type: pointer to gorm.DB
)

type ToDo struct{
    gorm.Model
    TaskName    string     `gorm:"not null" json:"title"`
    Description string     `gorm:"type text" json:"description"`
    Status      string     `gorm:"default:NotStarted" json:"status" validate:"required,oneof=NotStarted InProgress Pending Completed"`
    EndDate     *time.Time `json:"end_date"`
}

func ConnectDB() {...}
func InitDB(){...}

func ToDoHanlderCreate(w http.ResponseWriter, r *http.Request){
    // step1: Ensure it's a Post request
    if r.Method != http.MethodPost {
     http.Error(w, "Only Post method is allowed", http.StatusMethodNotAllowed)
    return
    }
    // Step 2: Parse the incoming JSON body into a ToDo struct
    var todo ToDo
    err := json.NewDecoder(r.Body).Decode(&todo)
    if err != nil {
        http.Error(w, "Invalid Json Input", http.StatusBadRequest)
        return
    }
    // Step 3: Save the new ToDo item to the database
    db.Create(&todo) // Using Gorm Native create object option
    // Step 4: Set the content type and return the created object as JSON
    w.Header().Set("Content-Type", "application/json") // Set response type as JSON
    w.WriteHeader(http.StatusCreated) // Set HTTP status code to 201 (Created)
    json.NewEncoder(w).Encode(todo) // Write the todo object as JSON in response


}

func main() {
    InitDB() // Initialize DB

    // Register the route and handler
    http.HandleFunc("/todo/create", ToDoHanlderCreate)

    fmt.Println("🚀 Server running at http://localhost:8080")
    err := http.ListenAndServe(":8080", nil)
    if err != nil {
        fmt.Println("Error starting server:", err)
}

Lets breakdown the ToDoHanlderCreate function and how we change main.

Step : 1 - ToDoHandlerFunction

  • func ToDoHanlderCreate(w http.ResponseWriter, r *http.Request){…}

    📥 r *http.Request

    • This parameter is used to receive and read the incoming HTTP request from the user.

    • It contains:

      • Request method (e.g., GET, POST)

      • Headers

      • Query parameters

      • Request body (like form data or JSON payload)

    • We're using a pointer (*http.Request) so we’re accessing the actual request data without copying it.

    • We store it in a variable named r so we can access its fields inside the function.

Example: To read the JSON request body, we'll use r.Body.

📤 w http.ResponseWriter

  • This parameter is used to send the response back to the user.

  • It lets us:

    • Write text or JSON data as the response

    • Set response status codes (like 200 OK or 400 Bad Request)

    • Set response headers (like Content-Type)

  • Anything you write to w is what the client receives.

Example: To send back a success message, we can use w.Write([]byte("ToDo created")).

Step : 2 - Json Decoding

    // Step 2: Parse the incoming JSON body into a ToDo struct
    var todo ToDo
    err := json.NewDecoder(r.Body).Decode(&todo)
    if err != nil {
        http.Error(w, "Invalid Json Input", http.StatusBadRequest)
        return
    }
  • Here, we create a variable called todo, which is of type ToDo struct. We need this variable because once we extract the content from the HTTP request body, we store it in this variable so we can use it later—for example, to insert it into the database.

  • err := json.NewDecoder(r.Body).Decode(&todo) - This line uses Go’s encoding/json package to decode the JSON data from the request body and assign it to the todo variable.

  • After decoding, we handle any potential error—this helps us ensure that the request body is in the correct format. If it's not, we can return a proper error message to the client.

Step : 3 - Create ToDo

db.Create(&todo) - We use GORM’s built-in Create method to add a new entry to our database. The data we’re inserting comes from the todo variable, which already holds the content we extracted and decoded from the incoming HTTP request. By passing &todo, we're telling GORM to insert the data from that memory location into the corresponding table.

Step : 4 - Response to user

w.Header().Set("Content-Type", "application/json") // Set response type as JSON
w.WriteHeader(http.StatusCreated)                 // Set HTTP status code to 201 (Created)
json.NewEncoder(w).Encode(todo)                   // Write the todo object as JSON in response
  • w.Header().Set("Content-Type", "application/json") - Tells the client that you're sending back JSON data.

  • w.WriteHeader(http.StatusCreated)- Sends an HTTP status code 201 Created, which is the correct status code for successful resource creation.

  • json.NewEncoder(w).Encode(todo) - Converts the todo struct to JSON and writes it to the response body.

main() Function:

InitDB() // Initialize DB

    // Register the route and handler
    http.HandleFunc("/todo/create", ToDoHanlderCreate)

    fmt.Println("🚀 Server running at http://localhost:8080")
    err := http.ListenAndServe(":8080", nil)
    if err != nil {
        fmt.Println("Error starting server:", err)
  • InitDB() This function initializes the database connection using GORM and MySQL. It also runs the AutoMigrate() function, which ensures that our ToDo model is translated into a table in the database.

  • http.HandleFunc("/todo/create", ToDoHandlerCreate) Registering the Route. This means that any POST request to /todo/create will be handled by the ToDoHandlerCreate function. This is the entry point for users to create new ToDo items.

  • http.ListenAndServe(":8080", nil) Starting the HTTP Server. This tells Go to start an HTTP server on port 8080. As long as the server is running, it will listen for incoming HTTP requests on that port.

✅ Summary

  • Database: Initialized and migrated with InitDB()

  • Route: /todo/create handles new ToDo creation

  • Server: Listens on http://localhost:8080

This covers the full operation of create ToDo item on our ToDo app using native http request. Here is the complete code snippet.

package main

import (
     "fmt"
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
    "time"
    "net/http"
    "encoding/json"


)

var db *gorm.DB // Global DB connection

type ToDo struct {
    gorm.Model
    TaskName    string     `gorm:"not null" json:"title"`
    Description string     `gorm:"type:text" json:"description"`
    Status      string     `gorm:"default:NotStarted" json:"status"`
    EndDate     *time.Time `json:"end_date"`
}

// ConnectDB sets up the MySQL connection and assigns it to the global db variable
func ConnectDB() {
    dsn := "root:password@tcp(127.0.0.1:3306)/todo?charset=utf8&parseTime=True&loc=Local"
    dbConn, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
    if err != nil {
        panic("Failed to connect to the database: " + err.Error())
    }
    db = dbConn
}

// InitDB initializes the DB and migrates the schema
func InitDB() {
    ConnectDB()
    db.AutoMigrate(&ToDo{}) // Creates the 'todos' table based on our struct
}

// CreateToDoHandler handles HTTP POST requests to create a new ToDo item
func ToDoHanlderCreate(w http.ResponseWriter, r *http.Request){
    // step1: Ensure it's a Post request
    if r.Method != http.MethodPost {
     http.Error(w, "Only Post method is allowed", http.StatusMethodNotAllowed)
    return
    }
    // Step 2: Parse the incoming JSON body into a ToDo struct
    var todo ToDo
    err := json.NewDecoder(r.Body).Decode(&todo)
    if err != nil {
        http.Error(w, "Invalid Json Input", http.StatusBadRequest)
        return
    }
    // Step 3: Save the new ToDo item to the database
    db.Create(&todo) // Using Gorm Native create object option
    // Step 4: Set the content type and return the created object as JSON
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(todo)

}

func main() {
    InitDB() // Initialize DB

    // Register the route and handler
    http.HandleFunc("/todo/create", ToDoHanlderCreate)

    fmt.Println("🚀 Server running at http://localhost:8080")
    err := http.ListenAndServe(":8080", nil)
    if err != nil {
        fmt.Println("Error starting server:", err)
    }  
}

▶️ How to test the code

  • Create Database if you don’t have one by following the steps mentioned here

  • Create a file called main.go and copy the above content and paste it.

  • Initialise your go app by running go mod init example.com/todo

  • Run go mod tidy to download all the dependencies

  • Finally run go run main.go start your application. You will see the below response on your terminal

  • Now open new terminal and use below curl command to create an entry in your ToDo app

curl -X POST http://localhost:8080/todo/create \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Write Go server",
    "description": "Build a simple ToDo API with Go and GORM",
    "status": "InProgress",
    "end_date": "2025-06-30T17:00:00Z"
  }'
  • You will get the response like this

  • To check it on database run the below commands
# Open CMD and run
docker exec -it mysql /bin/bash
mysql -u root -p
# <Enter your password when it asks i.e "password">
# Now you will be in Mysql db
USE todo;
SELECT * FROM to_dos
  • This command will show you the newly created Entry

📝 Conclusion

That wraps up our implementation of a simple ToDo API using Go’s net/http package, GORM, and MySQL. In this post, we created the model, set up the database connection, handled a POST request, and tested it with curl.

In the next blog post, we’ll explore alternatives to the net/http standard library—specifically, why developers often choose web frameworks like Gin or Echo for larger or more maintainable applications. We'll also complete the Application Layer, continuing our journey toward building a clean and modular Go backend.

Stay tuned!

0
Subscribe to my newsletter

Read articles from NaveenKumar VR directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

NaveenKumar VR
NaveenKumar VR