Skip to content

Building a Production REST API with PostgreSQL & Gin

A complete database-backed REST service supporting schema initialization, connection pooling, and JSON endpoints.

// Fundamentals learning: Building a database-backed API (Gin + PostgreSQL).
// To run:
// 1. Ensure Postgres is running locally.
// 2. Run the file:
//    go run basics_learning/4_postgres_db_api/main.go
// 3. Test:
//    POST: curl -X POST http://localhost:8080/tasks -H "Content-Type: application/json" -d '{"description": "Learn Go Database API"}'
//    GET:  curl http://localhost:8080/tasks

package main

import (
    "context"
    "database/sql"
    "log"
    "net/http"
    "os"
    "time"

    "github.com/gin-gonic/gin"
    _ "github.com/jackc/pgx/v5/stdlib"
)

type Task struct {
    ID          int       `json:"id"`
    Description string    `json:"description"`
    Done        bool      `json:"done"`
    CreatedAt   time.Time `json:"created_at"`
}

type CreateTaskReq struct {
    Description string `json:"description" binding:"required"`
}

func main() {
    // 1. Connect to PostgreSQL
    connStr := os.Getenv("DATABASE_URL")
    if connStr == "" {
        connStr = "postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable"
    }

    db, err := sql.Open("pgx", connStr)
    if err != nil {
        log.Fatalf("Failed to open database: %v", err)
    }
    defer db.Close()

    // Verify connection
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()
    if err := db.PingContext(ctx); err != nil {
        log.Fatalf("Postgres not available: %v", err)
    }

    // 2. Initialize the Database Table
    createTableSQL := `
    CREATE TABLE IF NOT EXISTS learning_tasks (
        id SERIAL PRIMARY KEY,
        description TEXT NOT NULL,
        done BOOLEAN DEFAULT FALSE,
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    );`
    if _, err := db.ExecContext(ctx, createTableSQL); err != nil {
        log.Fatalf("Failed to initialize database table: %v", err)
    }
    log.Println("Database initialized successfully.")

    // 3. Initialize Gin Router
    r := gin.Default()

    // 4. API Endpoint: Create a Task (POST /tasks)
    r.POST("/tasks", func(c *gin.Context) {
        var req CreateTaskReq
        if err := c.ShouldBindJSON(&req); err != nil {
            c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
            return
        }

        insertSQL := `
            INSERT INTO learning_tasks (description) 
            VALUES ($1) 
            RETURNING id, done, created_at;`

        var task Task
        task.Description = req.Description

        // Execute query and scan returning values into the task struct
        err := db.QueryRowContext(
            c.Request.Context(), // Use the request's context so the DB query cancels if the client disconnects!
            insertSQL,
            req.Description,
        ).Scan(&task.ID, &task.Done, &task.CreatedAt)

        if err != nil {
            c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save task: " + err.Error()})
            return
        }

        c.JSON(http.StatusCreated, task)
    })

    // 5. API Endpoint: List all Tasks (GET /tasks)
    r.GET("/tasks", func(c *gin.Context) {
        selectSQL := `SELECT id, description, done, created_at FROM learning_tasks ORDER BY id DESC;`

        rows, err := db.QueryContext(c.Request.Context(), selectSQL)
        if err != nil {
            c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch tasks: " + err.Error()})
            return
        }
        defer rows.Close()

        tasks := []Task{} // Initialize empty slice so it serializes as "[]" instead of "null" if empty
        for rows.Next() {
            var task Task
            err := rows.Scan(&task.ID, &task.Description, &task.Done, &task.CreatedAt)
            if err != nil {
                c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to parse task: " + err.Error()})
                return
            }
            tasks = append(tasks, task)
        }

        c.JSON(http.StatusOK, tasks)
    })

    // 6. Start Server
    log.Println("Starting Gin + Postgres server on :8080...")
    if err := r.Run(":8080"); err != nil {
        log.Fatalf("Server failed: %v", err)
    }
}