Guides/ Go

Gin with PostgreSQL and GORM

Create a Go API project with Gin, PostgreSQL, GORM, Zap logging, and a clean Better Fullstack project structure.

Updated 2026-05-12

ginpostgresgormgo

Use this stack when you want a Go HTTP API with a familiar router, relational database access, and pragmatic project structure.

npm create better-fullstack@latest my-go-api -- \
  --ecosystem go \
  --go-web-framework gin \
  --database postgres \
  --go-orm gorm \
  --go-logging zap

Generated stack snapshot

The generated Go service is organized around these responsibilities.

HTTP
Gin
Routing, request binding, middleware, and responses.
Data
PostgreSQL + GORM
Model mapping and persistence for CRUD-heavy services.
Logging
Zap
Structured logs that can carry request and dependency context.
Shape
cmd + internal
A backend service layout that keeps app code behind package boundaries.

What this creates

  • A Go API project using Gin.
  • PostgreSQL as the database option.
  • GORM for database access.
  • Zap for structured logging.
  • A generated project layout ready for backend development.

Generated shape

This stack creates a conventional Go HTTP API. Gin handles routing and middleware, GORM owns model mapping and persistence, PostgreSQL is the backing store, and Zap provides structured logs.

The cleanest generated-app boundary is:

  • HTTP handlers parse input and return responses.
  • Services own business decisions.
  • GORM models describe persisted records.
  • Database setup and logger setup happen once during startup.

Representative file tree

my-go-api/
  go.mod
  .env.example
  cmd/
    server/
      main.go
  internal/
    config/
      config.go
    database/
      postgres.go
    handlers/
      health.go
      users.go
    models/
      user.go
    services/
      users.go

Generated Go projects vary by option set, but this is the shape to preserve as the app grows: keep framework code near handlers and persistence code behind services or repositories.

Handler and model examples

A Gin handler should bind request data, call application code, and return a clear HTTP status:

package handlers

import (
	"net/http"

	"github.com/gin-gonic/gin"
)

type CreateUserRequest struct {
	Email string `json:"email" binding:"required,email"`
	Name  string `json:"name" binding:"required"`
}

func (h *UserHandler) Create(c *gin.Context) {
	var input CreateUserRequest
	if err := c.ShouldBindJSON(&input); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	user, err := h.Users.Create(c.Request.Context(), input.Email, input.Name)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "create user"})
		return
	}

	c.JSON(http.StatusCreated, user)
}

The matching GORM model can stay small and explicit:

package models

import "time"

type User struct {
	ID        uint      `json:"id" gorm:"primaryKey"`
	Email     string    `json:"email" gorm:"uniqueIndex;not null"`
	Name      string    `json:"name" gorm:"not null"`
	CreatedAt time.Time `json:"createdAt"`
	UpdatedAt time.Time `json:"updatedAt"`
}

Route registration should keep related endpoints together:

func RegisterRoutes(router *gin.Engine, users *UserHandler) {
	router.GET("/health", Health)

	api := router.Group("/api")
	api.POST("/users", users.Create)
	api.GET("/users/:id", users.Get)
}

Compatibility notes

When to choose it

Choose Gin when you want a direct Go API with common middleware patterns and low framework overhead.

Tradeoffs

Gin and GORM are familiar choices for many Go teams. If you prefer a smaller router surface or different API style, compare the other Go framework options in the builder.

GORM is productive for CRUD-heavy applications, but it can hide SQL details. For query-heavy services where exact SQL is part of the contract, compare the Echo with SQLC guide.

Testing and deployment notes

Use httptest for handler tests and a real PostgreSQL database for integration tests around GORM behavior.

go test ./...

Before deployment, confirm the generated app reads the database URL from the environment and that migrations or auto-migration behavior match your team's release policy. Use Zap fields for request IDs, user IDs, and dependency failures so production logs can be filtered without string parsing.

Troubleshooting

  • If requests return 400, inspect Gin binding tags before debugging service code.
  • If inserts fail with duplicate key errors, check GORM indexes and PostgreSQL constraints together.
  • If the app cannot connect to PostgreSQL, verify host, port, SSL mode, and the generated environment variable names.
  • If logs are unstructured, make sure the Zap logger is passed into handlers and services instead of using fmt.Println.

Next steps