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.
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?

Performance Comparison:
| Language | Requests/sec | Memory |
|---|---|---|
| Go | 150,000 | 10 MB |
| Node.js | 45,000 | 120 MB |
| Python | 8,000 | 200 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:

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
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /health | No | Health check |
| POST | /api/v1/register | No | Create user |
| GET | /api/v1/users | Yes | List all users |
| GET | /api/v1/users/:id | Yes | Get user by ID |
| PUT | /api/v1/users/:id | Yes | Update user |
| DELETE | /api/v1/users/:id | Yes | Delete user |
Best Practices
- Project Layout: Follow Go’s standard project layout
- Error Handling: Always check and handle errors
- Validation: Use Gin’s binding for input validation
- Passwords: Always hash with bcrypt
- Environment: Use .env for configuration
- 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:
- Add login endpoint with JWT generation
- Implement rate limiting
- Add logging middleware
- Write unit tests
Go is fast, simple, and perfect for APIs. Start using it today!
Resources
Published: December 7, 2024
Advertisement
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
FastAPI Tutorial: Build Modern Python APIs
Master FastAPI for building high-performance Python APIs. Learn async endpoints, validation, authentication, database integration, and deployment.
Spring BootREST API Design Best Practices: Complete Guide
Master REST API design with industry best practices. Learn naming conventions, versioning, error handling, pagination, and security patterns.
JavaScriptSupabase: Complete Backend-as-a-Service Guide
Master Supabase for full-stack development. Learn PostgreSQL database, authentication, real-time subscriptions, storage, and build modern applications.
Comments
Comments are powered by GitHub Discussions.
Configure Giscus at giscus.app to enable comments.