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
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 zapGenerated 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.goGenerated 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
- Open the Stack Builder.
- Read the Go ecosystem docs.
- Review the CLI create reference.