A production-ready Go API starter repository with a clean architecture design, implementing best practices for building scalable, maintainable web services.
This repository provides a structured foundation for building robust REST APIs in Go. It follows clean architecture principles to ensure separation of concerns, testability, and maintainability.
Key features:
- Structured project layout with domain-driven design
- Configuration management with environment variables
- PostgreSQL integration with migrations
- Chi router with middleware setup
- JWT authentication
- Structured logging with slog
- Graceful shutdown handling
The project follows a layered architecture:
go-api-structure/
├── cmd/api/ # Application entrypoint
├── internal/ # Private application packages
│ ├── api/ # HTTP handlers and API utilities
│ ├── auth/ # Authentication logic
│ ├── config/ # Configuration management
│ ├── database/ # Database connection management
│ ├── logger/ # Logging setup
│ ├── server/ # HTTP server implementation
│ └── store/ # Data access layer
├── migrations/ # Database migration files
└── sqlc.yaml # SQLC configuration
- Separation of Concerns: Each component has a specific responsibility
- Dependency Injection: Dependencies are passed explicitly, making testing easier
- Interface-Driven Design: Core business logic depends on interfaces, not implementations
- Error Handling: Consistent error handling throughout the application
- Context Propagation: Context is used for cancellation, timeouts, and tracing
- Language: Go 1.21+
- Database: PostgreSQL
- Web Framework: Chi router
- SQL Generation: SQLC for type-safe database access
- Authentication: JWT with bcrypt password hashing
- Logging: slog structured logging
- Migration: SQL migration files
- Go 1.21+
- PostgreSQL
- SQLC CLI
- Make (optional, for using Makefile shortcuts)
- Create a PostgreSQL database:
createdb go_api_db- Run migrations:
# Install migrate CLI if needed
go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest
# Run migrations
migrate -path migrations -database "postgres://localhost:5432/go_api_db?sslmode=disable" upCreate a .env file in the project root with the following variables:
APP_ENV=development
HTTP_PORT=8080
DATABASE_DSN=postgres://postgres:postgres@localhost:5432/go_api_db?sslmode=disable
JWT_SECRET=your_very_secure_jwt_secret_key
JWT_EXPIRY_DURATION=24h
go run cmd/api/main.goThis project uses Lefthook to manage Git hooks. Lefthook helps automate tasks like generating Swagger documentation before committing.
-
Install Lefthook: If you don't have Lefthook installed, you can install it via Go:
go install github.com/evilmartians/lefthook/cmd/lefthook@latest
Alternatively, use Homebrew on macOS:
brew install lefthook
For other installation methods, see the Lefthook documentation.
-
Install Hooks: After installing Lefthook, navigate to the project root directory and run:
lefthook install
This will set up the pre-commit hooks defined in
lefthook.yml. Now, when you commit changes to Go files, Swagger documentation will be automatically regenerated and staged.
To add a new entity (e.g., Product):
-
Create SQL Migration:
-- in migrations/YYYYMMDDHHMMSS_create_products_table.up.sql CREATE TABLE products ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL, description TEXT, price INTEGER NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- in migrations/YYYYMMDDHHMMSS_create_products_table.down.sql DROP TABLE IF EXISTS products;
-
Define SQLC Queries:
-- in internal/store/query/product.sql -- name: CreateProduct :one INSERT INTO products (name, description, price) VALUES ($1, $2, $3) RETURNING *; -- name: GetProduct :one SELECT * FROM products WHERE id = $1; -- name: ListProducts :many SELECT * FROM products ORDER BY created_at DESC LIMIT $1 OFFSET $2;
-
Generate SQLC Code:
sqlc generate
-
Create Store Interface:
// in internal/store/product.go package store import ( "context" "go-api-structure/internal/store/db" ) type ProductStore interface { CreateProduct(ctx context.Context, params db.CreateProductParams) (db.Product, error) GetProduct(ctx context.Context, id uuid.UUID) (db.Product, error) ListProducts(ctx context.Context, params db.ListProductsParams) ([]db.Product, error) }
-
Add to Store Interface:
// in internal/store/store.go type Store interface { UserStore ProductStore // other stores... }
-
Create DTOs:
// in internal/api/dto/product.go package dto type CreateProductRequest struct { Name string `json:"name" validate:"required"` Description string `json:"description"` Price int `json:"price" validate:"required,gt=0"` } type ProductResponse struct { ID string `json:"id"` Name string `json:"name"` Description string `json:"description"` Price int `json:"price"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` }
-
Create Handler:
// in internal/api/handler_product.go package api // ProductHandler handles HTTP requests related to products type ProductHandler struct { store store.ProductStore } // NewProductHandler creates a new product handler func NewProductHandler(store store.ProductStore) *ProductHandler { return &ProductHandler{ store: store, } } // CreateProduct handles creating a new product func (h *ProductHandler) CreateProduct(w http.ResponseWriter, r *http.Request) { // Implementation... } // GetProduct handles retrieving a product by ID func (h *ProductHandler) GetProduct(w http.ResponseWriter, r *http.Request) { // Implementation... } // ListProducts handles listing products with pagination func (h *ProductHandler) ListProducts(w http.ResponseWriter, r *http.Request) { // Implementation... }
-
Update Server:
// in internal/server/server.go type Server struct { // Existing fields... productHandler *api.ProductHandler } func NewServer(cfg *config.Config, logger *slog.Logger, store store.Store) http.Handler { s := &Server{ // Existing initialization... productHandler: api.NewProductHandler(store), } // Rest of the function... } // in addRoutes method, add: s.router.Route("/api/v1/products", func(r chi.Router) { r.Use(s.authService.Middleware(api.ErrorResponse)) r.Post("/", s.productHandler.CreateProduct) r.Get("/{productID}", s.productHandler.GetProduct) r.Get("/", s.productHandler.ListProducts) })
- Keep entity logic contained in dedicated files/packages
- Follow the existing patterns for consistency
- Add validation for all incoming data
- Maintain proper error handling and logging
- Write tests for new components