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


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:
Start with the basic (native) approach to understand how things work at the core level.
Evaluate and adopt a better alternative library that simplifies and enhances the process.
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:
Define an HTTP handler function
Read and parse the JSON body from the client
Validate and use the data to create a new ToDo
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:
Receive incoming HTTP requests
Read the data (like a new ToDo item) from the request body
Pass it to our business logic (e.g.,
CreateToDo
)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 typeToDo
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’sencoding/json
package to decode the JSON data from the request body and assign it to thetodo
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 thetodo
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 anyPOST
request to/todo/create
will be handled by theToDoHandlerCreate
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 port8080
. 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 creationServer: 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 dependenciesFinally run
go run main.go
start your application. You will see the below response on your terminalNow 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!
Subscribe to my newsletter
Read articles from NaveenKumar VR directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
