This episode guides you step by step in building your first Gin project: module initialization, installing dependencies, understanding gin.New() vs gin.Default(), debug and release modes, creating your first routes, and running the server.

Now it's time to write real code. Episode 3 is the point where you build your first actually working Gin project: from an empty directory, Go module initialization, installing Gin, to a server that responds to your first endpoint. This is your operational foundation — every later episode builds on this structure, so understand each step rather than just copying it.
Start by creating a directory and a Go module:
mkdir hello-gin
cd hello-gin
go mod init hello-ginThe command go mod init hello-gin creates the go.mod file that records the module name and Go version. Next, install Gin:
go get github.com/gin-gonic/gin@v1.12.0
go mod tidygo mod tidy cleans up dependencies: it removes unused ones and makes sure go.sum is complete. Now the minimal project structure is go.mod, go.sum, and the Go file we're about to create.
There are two ways to create an Engine, and the difference matters:
gin.New(): an empty engine with no middleware at all.gin.Default(): an engine with gin.Logger() and gin.Recovery() already installed.r1 := gin.New()
r2 := gin.Default()
// r1 tidak punya middleware apapun
// r2 punya Logger + RecoveryFor learning, gin.Default() is the most convenient choice because you immediately get request logging and panic protection. In production, you can use gin.New() and add more controlled middleware — covered in episode 6.
Gin uses debug mode by default, marked by logs like [GIN-debug] GET /hello --> main.main.func1. For release mode, set the environment variable or call gin.SetMode:
GIN_MODE=release go run main.gogin.SetMode(gin.ReleaseMode)
r := gin.Default()Release mode disables route logs and some debug checks, making it faster and leaner in production.
Gin provides GET, POST, PUT, PATCH, DELETE methods directly on the Engine. Let's write a complete main.go file:
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.GET("/hello", func(c *gin.Context) {
c.String(http.StatusOK, "hello world")
})
r.GET("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "pong"})
})
r.Run(":8080")
}c.String(http.StatusOK, "hello world") sends plain text, while c.JSON sends a JSON object. gin.H is a convenient alias for map[string]any used for simple responses.
go run main.gocurl http://localhost:8080/hello
curl http://localhost:8080/pingThe output is hello world and {"message":"pong"}. The function r.Run(":8080") calls http.ListenAndServe(":8080", r) — the Gin Engine satisfies the http.Handler interface, which is why Gin can be plugged into the standard net/http.
The GET endpoints are working. Now add a POST endpoint that accepts JSON, then register its handler with r.POST("/todo", createTodo):
type Todo struct {
Title string `json:"title"`
}
func createTodo(c *gin.Context) {
var todo Todo
if err := c.ShouldBindJSON(&todo); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{"title": todo.Title})
}c.ShouldBindJSON(&todo) maps the JSON body to the Todo struct according to the json:"title" tag. If the format is wrong, we return status 400.
curl -X POST http://localhost:8080/todo \
-H "Content-Type: application/json" \
-d '{"title":"belajar gin"}'The curl command above sends the Content-Type: application/json header and a JSON body. This is the standard REST API pattern you'll use over and over.
To close the episode, combine the HTTP methods in one small application. We'll simulate storage with an in-memory slice (the real database comes in episode 9):
var todos = []gin.H{}
func main() {
r := gin.Default()
r.GET("/todos", func(c *gin.Context) {
c.JSON(http.StatusOK, todos)
})
r.POST("/todos", func(c *gin.Context) {
var body gin.H
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
todos = append(todos, body)
c.JSON(http.StatusCreated, body)
})
r.Run(":8080")
}Path parameters like :index will be covered fully in episode 4. Notice the common pattern: every handler receives *gin.Context, reads input, processes it, then writes a response.
Key takeaways:
go mod init then go get github.com/gin-gonic/gin@v1.12.0.gin.Default() = Engine + Logger + Recovery; gin.New() is empty.GIN_MODE=release disables debug logs.c.String, c.JSON, and c.ShouldBindJSON are the first APIs you must master.r.Run(":8080") runs the server because the Engine satisfies http.Handler.In the next episode, episode 4, we'll dissect routing & path parameters — the /users/:id pattern, wildcards like /files/*filepath, query parameters, RouterGroups with v1 and v2 prefixes, and handling 404 and 405 responses.