Go 9 min read

Golang REST API 2025: Complete Production Guide with Gin & PostgreSQL

Build production-ready REST APIs with Go, Gin framework, PostgreSQL, and JWT authentication. Step-by-step tutorial with complete code examples.

MR

Moshiour Rahman

Advertisement

Golang REST API 2025: Complete Production Guide

Go is the fastest-growing backend language in 2025, and for good reason. Companies like Google, Uber, Dropbox, and Netflix use Go for their most critical services. In this guide, you’ll build a production-ready REST API from scratch.

Why Go for APIs?

Go Language Features

Performance Comparison:

LanguageRequests/secMemory
Go150,00010 MB
Node.js45,000120 MB
Python8,000200 MB

Companies Using Go:

  • Google (created Go)
  • Uber (microservices)
  • Dropbox (infrastructure)
  • Netflix (server tools)
  • Docker (entire platform)

What You’ll Build

A complete REST API with:

  • ✅ CRUD operations for users
  • ✅ PostgreSQL database
  • ✅ JWT authentication
  • ✅ Input validation
  • ✅ Docker deployment

Project Structure:

Go Project Structure

Prerequisites

  • Go 1.21+ installed
  • PostgreSQL (or use Docker)
  • VS Code with Go extension

Step 1: Setup Go Project

Install Go

# macOS
brew install go

# Linux
sudo apt install golang-go

# Verify installation
go version

Create Project

mkdir golang-rest-api && cd golang-rest-api
go mod init github.com/yourusername/golang-rest-api

Install Dependencies

go get github.com/gin-gonic/gin
go get gorm.io/gorm
go get gorm.io/driver/postgres
go get github.com/golang-jwt/jwt/v5
go get github.com/joho/godotenv
go get golang.org/x/crypto

Step 2: Go Fundamentals (Quick Primer)

Variables & Types

package main

import "fmt"

func main() {
    // Variable declaration
    var name string = "TechyOwls"
    age := 25 // Type inference
    isActive := true

    // Arrays and slices
    numbers := []int{1, 2, 3, 4, 5}
    
    // Maps
    user := map[string]string{
        "name":  "John",
        "email": "john@example.com",
    }

    fmt.Println(name, age, isActive)
    fmt.Println(numbers, user)
}

Structs and Methods

// Define a struct
type User struct {
    ID    int
    Name  string
    Email string
}

// Method on struct
func (u *User) FullInfo() string {
    return fmt.Sprintf("%s (%s)", u.Name, u.Email)
}

// Usage
user := User{ID: 1, Name: "John", Email: "john@example.com"}
fmt.Println(user.FullInfo())

Error Handling

func divide(a, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("cannot divide by zero")
    }
    return a / b, nil
}

result, err := divide(10, 2)
if err != nil {
    log.Fatal(err)
}
fmt.Println(result) // 5

Concurrency with Goroutines

// Run functions concurrently
go fetchData("url1")
go fetchData("url2")

// Channels for communication
ch := make(chan string)
go func() {
    ch <- "Hello from goroutine"
}()
message := <-ch // Wait for message

Step 3: Project Structure

Create the following structure:

golang-rest-api/
├── main.go
├── go.mod
├── .env
├── internal/
│   ├── handlers/
│   │   ├── user_handler.go
│   │   └── health_handler.go
│   ├── models/
│   │   └── user.go
│   ├── middleware/
│   │   └── auth.go
│   └── routes/
│       └── routes.go
└── pkg/
    └── database/
        └── postgres.go

Step 4: Database Connection

pkg/database/postgres.go

package database

import (
    "fmt"
    "os"

    "gorm.io/driver/postgres"
    "gorm.io/gorm"
)

func Connect() (*gorm.DB, error) {
    dsn := fmt.Sprintf(
        "host=%s user=%s password=%s dbname=%s port=%s sslmode=disable",
        getEnv("DB_HOST", "localhost"),
        getEnv("DB_USER", "postgres"),
        getEnv("DB_PASSWORD", "postgres"),
        getEnv("DB_NAME", "golang_api"),
        getEnv("DB_PORT", "5432"),
    )

    db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
    if err != nil {
        return nil, err
    }

    return db, nil
}

func getEnv(key, defaultValue string) string {
    if value := os.Getenv(key); value != "" {
        return value
    }
    return defaultValue
}

Step 5: User Model

internal/models/user.go

package models

import (
    "time"
    "gorm.io/gorm"
)

type User struct {
    ID        uint           `json:"id" gorm:"primaryKey"`
    Name      string         `json:"name" gorm:"size:100;not null"`
    Email     string         `json:"email" gorm:"uniqueIndex;not null"`
    Password  string         `json:"-" gorm:"not null"`
    CreatedAt time.Time      `json:"created_at"`
    UpdatedAt time.Time      `json:"updated_at"`
    DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
}

// Request/Response types
type CreateUserRequest struct {
    Name     string `json:"name" binding:"required,min=2"`
    Email    string `json:"email" binding:"required,email"`
    Password string `json:"password" binding:"required,min=6"`
}

type UserResponse struct {
    ID        uint      `json:"id"`
    Name      string    `json:"name"`
    Email     string    `json:"email"`
    CreatedAt time.Time `json:"created_at"`
}

func (u *User) ToResponse() UserResponse {
    return UserResponse{
        ID:        u.ID,
        Name:      u.Name,
        Email:     u.Email,
        CreatedAt: u.CreatedAt,
    }
}

Step 6: CRUD Handlers

internal/handlers/user_handler.go

package handlers

import (
    "net/http"
    "strconv"

    "github.com/gin-gonic/gin"
    "golang.org/x/crypto/bcrypt"
    "gorm.io/gorm"
    "your-module/internal/models"
)

type UserHandler struct {
    db *gorm.DB
}

func NewUserHandler(db *gorm.DB) *UserHandler {
    return &UserHandler{db: db}
}

// GET /users - Get all users
func (h *UserHandler) GetAllUsers(c *gin.Context) {
    var users []models.User
    if err := h.db.Find(&users).Error; err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{
            "error": "Failed to fetch users",
        })
        return
    }

    response := make([]models.UserResponse, len(users))
    for i, user := range users {
        response[i] = user.ToResponse()
    }

    c.JSON(http.StatusOK, gin.H{"data": response})
}

// GET /users/:id - Get user by ID
func (h *UserHandler) GetUserByID(c *gin.Context) {
    id, err := strconv.Atoi(c.Param("id"))
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{
            "error": "Invalid user ID",
        })
        return
    }

    var user models.User
    if err := h.db.First(&user, id).Error; err != nil {
        c.JSON(http.StatusNotFound, gin.H{
            "error": "User not found",
        })
        return
    }

    c.JSON(http.StatusOK, gin.H{"data": user.ToResponse()})
}

// POST /register - Create new user
func (h *UserHandler) CreateUser(c *gin.Context) {
    var req models.CreateUserRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{
            "error": err.Error(),
        })
        return
    }

    // Hash password
    hashedPassword, _ := bcrypt.GenerateFromPassword(
        []byte(req.Password), 
        bcrypt.DefaultCost,
    )

    user := models.User{
        Name:     req.Name,
        Email:    req.Email,
        Password: string(hashedPassword),
    }

    if err := h.db.Create(&user).Error; err != nil {
        c.JSON(http.StatusConflict, gin.H{
            "error": "Email already exists",
        })
        return
    }

    c.JSON(http.StatusCreated, gin.H{"data": user.ToResponse()})
}

// PUT /users/:id - Update user
func (h *UserHandler) UpdateUser(c *gin.Context) {
    id, _ := strconv.Atoi(c.Param("id"))
    
    var user models.User
    if err := h.db.First(&user, id).Error; err != nil {
        c.JSON(http.StatusNotFound, gin.H{
            "error": "User not found",
        })
        return
    }

    var req struct {
        Name  string `json:"name"`
        Email string `json:"email"`
    }
    c.ShouldBindJSON(&req)

    if req.Name != "" {
        user.Name = req.Name
    }
    if req.Email != "" {
        user.Email = req.Email
    }

    h.db.Save(&user)
    c.JSON(http.StatusOK, gin.H{"data": user.ToResponse()})
}

// DELETE /users/:id - Delete user
func (h *UserHandler) DeleteUser(c *gin.Context) {
    id, _ := strconv.Atoi(c.Param("id"))
    
    if err := h.db.Delete(&models.User{}, id).Error; err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{
            "error": "Failed to delete user",
        })
        return
    }

    c.JSON(http.StatusOK, gin.H{
        "message": "User deleted successfully",
    })
}

Step 7: JWT Authentication

internal/middleware/auth.go

package middleware

import (
    "net/http"
    "os"
    "strings"

    "github.com/gin-gonic/gin"
    "github.com/golang-jwt/jwt/v5"
)

func AuthMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        // Get Authorization header
        authHeader := c.GetHeader("Authorization")
        if authHeader == "" {
            c.JSON(http.StatusUnauthorized, gin.H{
                "error": "Authorization header required",
            })
            c.Abort()
            return
        }

        // Extract token from "Bearer <token>"
        parts := strings.Split(authHeader, " ")
        if len(parts) != 2 || parts[0] != "Bearer" {
            c.JSON(http.StatusUnauthorized, gin.H{
                "error": "Invalid authorization format",
            })
            c.Abort()
            return
        }

        // Verify token
        tokenString := parts[1]
        secret := os.Getenv("JWT_SECRET")
        
        token, err := jwt.Parse(tokenString, func(t *jwt.Token) (interface{}, error) {
            return []byte(secret), nil
        })

        if err != nil || !token.Valid {
            c.JSON(http.StatusUnauthorized, gin.H{
                "error": "Invalid token",
            })
            c.Abort()
            return
        }

        // Set user ID in context
        if claims, ok := token.Claims.(jwt.MapClaims); ok {
            c.Set("userID", claims["user_id"])
        }

        c.Next()
    }
}

Step 8: Routes Setup

internal/routes/routes.go

package routes

import (
    "github.com/gin-gonic/gin"
    "your-module/internal/handlers"
    "your-module/internal/middleware"
)

func SetupRoutes(
    r *gin.Engine, 
    userHandler *handlers.UserHandler,
    healthHandler *handlers.HealthHandler,
) {
    // Health check (public)
    r.GET("/health", healthHandler.Health)

    // API v1 group
    v1 := r.Group("/api/v1")
    {
        // Public routes
        v1.POST("/register", userHandler.CreateUser)

        // Protected routes
        protected := v1.Group("/")
        protected.Use(middleware.AuthMiddleware())
        {
            protected.GET("/users", userHandler.GetAllUsers)
            protected.GET("/users/:id", userHandler.GetUserByID)
            protected.PUT("/users/:id", userHandler.UpdateUser)
            protected.DELETE("/users/:id", userHandler.DeleteUser)
        }
    }
}

Step 9: Main Entry Point

main.go

package main

import (
    "log"
    "os"

    "github.com/gin-gonic/gin"
    "github.com/joho/godotenv"
    "your-module/internal/handlers"
    "your-module/internal/models"
    "your-module/internal/routes"
    "your-module/pkg/database"
)

func main() {
    // Load .env file
    godotenv.Load()

    // Connect to database
    db, err := database.Connect()
    if err != nil {
        log.Fatal("Database connection failed:", err)
    }

    // Run migrations
    db.AutoMigrate(&models.User{})

    // Initialize handlers
    userHandler := handlers.NewUserHandler(db)
    healthHandler := handlers.NewHealthHandler()

    // Setup router
    router := gin.Default()
    routes.SetupRoutes(router, userHandler, healthHandler)

    // Start server
    port := os.Getenv("PORT")
    if port == "" {
        port = "8080"
    }

    log.Printf("🚀 Server starting on port %s", port)
    router.Run(":" + port)
}

Step 10: Environment Variables

.env

PORT=8080
DB_HOST=localhost
DB_USER=postgres
DB_PASSWORD=postgres
DB_NAME=golang_api
DB_PORT=5432
JWT_SECRET=your-super-secret-key

Step 11: Docker Deployment

Dockerfile

FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o main .

FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/main .
EXPOSE 8080
CMD ["./main"]

docker-compose.yml

version: '3.8'

services:
  api:
    build: .
    ports:
      - "8080:8080"
    environment:
      - DB_HOST=db
      - DB_USER=postgres
      - DB_PASSWORD=postgres
      - DB_NAME=golang_api
      - JWT_SECRET=secret
    depends_on:
      - db

  db:
    image: postgres:15-alpine
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
      - POSTGRES_DB=golang_api
    ports:
      - "5432:5432"

Run with Docker:

docker-compose up -d

Step 12: Testing the API

Health Check

curl http://localhost:8080/health
# {"status":"ok","message":"Go REST API is running"}

Create User

curl -X POST http://localhost:8080/api/v1/register \
  -H "Content-Type: application/json" \
  -d '{"name":"John Doe","email":"john@example.com","password":"secret123"}'

Get Users (with JWT)

curl http://localhost:8080/api/v1/users \
  -H "Authorization: Bearer YOUR_JWT_TOKEN"

API Endpoints Summary

MethodEndpointAuthDescription
GET/healthNoHealth check
POST/api/v1/registerNoCreate user
GET/api/v1/usersYesList all users
GET/api/v1/users/:idYesGet user by ID
PUT/api/v1/users/:idYesUpdate user
DELETE/api/v1/users/:idYesDelete user

Best Practices

  1. Project Layout: Follow Go’s standard project layout
  2. Error Handling: Always check and handle errors
  3. Validation: Use Gin’s binding for input validation
  4. Passwords: Always hash with bcrypt
  5. Environment: Use .env for configuration
  6. Docker: Deploy with multi-stage builds

Code Repository

🎁 Get the complete code:

golang-rest-api-guide on GitHub

git clone https://github.com/Moshiour027/techyowls-io-blog-public.git
cd techyowls-io-blog-public/golang-rest-api-guide
docker-compose up -d

Conclusion

You’ve built a production-ready Go REST API with:

  • ✅ Gin web framework
  • ✅ PostgreSQL with GORM
  • ✅ JWT authentication
  • ✅ Input validation
  • ✅ Docker deployment

Next Steps:

  1. Add login endpoint with JWT generation
  2. Implement rate limiting
  3. Add logging middleware
  4. Write unit tests

Go is fast, simple, and perfect for APIs. Start using it today!


Resources


Published: December 7, 2024

Advertisement

MR

Moshiour Rahman

Software Architect & AI Engineer

Share:
MR

Moshiour Rahman

Software Architect & AI Engineer

Enterprise software architect with deep expertise in financial systems, distributed architecture, and AI-powered applications. Building large-scale systems at Fortune 500 companies. Specializing in LLM orchestration, multi-agent systems, and cloud-native solutions. I share battle-tested patterns from real enterprise projects.

Related Articles

Comments

Comments are powered by GitHub Discussions.

Configure Giscus at giscus.app to enable comments.