Bookmark
Golang Web API Template
main.go
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/gin-gonic/gin"
"golang-app/internal/config"
"golang-app/internal/handlers"
"golang-app/internal/middleware"
"golang-app/internal/repository"
"golang-app/internal/service"
)
func main() {
// Load configuration
cfg, err := config.Load()
if err != nil {
log.Fatal("Failed to load config:", err)
}
// Initialize repository layer
repo := repository.NewMemoryRepository()
// Initialize service layer
svc := service.New(repo)
// Initialize handlers
h := handlers.New(svc)
// Setup Gin router
if cfg.Environment == "production" {
gin.SetMode(gin.ReleaseMode)
}
router := gin.New()
router.Use(gin.Logger())
router.Use(gin.Recovery())
router.Use(middleware.CORS())
router.Use(middleware.RequestID())
// Health check endpoint
router.GET("/health", h.HealthCheck)
// API routes
v1 := router.Group("/api/v1")
{
v1.GET("/users", h.GetUsers)
v1.POST("/users", h.CreateUser)
v1.GET("/users/:id", h.GetUser)
v1.PUT("/users/:id", h.UpdateUser)
v1.DELETE("/users/:id", h.DeleteUser)
}
// Create HTTP server
server := &http.Server{
Addr: fmt.Sprintf(":%s", cfg.Port),
Handler: router,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}
// Start server in a goroutine
go func() {
log.Printf("Server starting on port %s", cfg.Port)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal("Failed to start server:", err)
}
}()
// Wait for interrupt signal to gracefully shutdown the server
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutting down server...")
// Graceful shutdown with timeout
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Fatal("Server forced to shutdown:", err)
}
log.Println("Server exited")
}
internal/config/config.go
package config
import (
"os"
"strconv"
)
// Config holds the application configuration
type Config struct {
Port string
Environment string
LogLevel string
Database DatabaseConfig
}
// DatabaseConfig holds database configuration
type DatabaseConfig struct {
Host string
Port int
Username string
Password string
Database string
SSLMode string
}
// Load loads configuration from environment variables with defaults
func Load() (*Config, error) {
cfg := &Config{
Port: getEnv("PORT", "8080"),
Environment: getEnv("ENVIRONMENT", "development"),
LogLevel: getEnv("LOG_LEVEL", "info"),
Database: DatabaseConfig{
Host: getEnv("DB_HOST", "localhost"),
Port: getEnvAsInt("DB_PORT", 5432),
Username: getEnv("DB_USERNAME", "postgres"),
Password: getEnv("DB_PASSWORD", ""),
Database: getEnv("DB_DATABASE", "golang_app"),
SSLMode: getEnv("DB_SSL_MODE", "disable"),
},
}
return cfg, nil
}
// getEnv gets an environment variable or returns a default value
func getEnv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
// getEnvAsInt gets an environment variable as integer or returns a default value
func getEnvAsInt(key string, defaultValue int) int {
if valueStr := os.Getenv(key); valueStr != "" {
if value, err := strconv.Atoi(valueStr); err == nil {
return value
}
}
return defaultValue
}
internal/models/user.go
package models
import (
"time"
"github.com/google/uuid"
)
// User represents a user in the system
type User struct {
ID string `json:"id" validate:"required,uuid"`
FirstName string `json:"first_name" validate:"required,min=2,max=50"`
LastName string `json:"last_name" validate:"required,min=2,max=50"`
Email string `json:"email" validate:"required,email"`
Age int `json:"age" validate:"required,min=1,max=120"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// CreateUserRequest represents the request payload for creating a user
type CreateUserRequest struct {
FirstName string `json:"first_name" validate:"required,min=2,max=50"`
LastName string `json:"last_name" validate:"required,min=2,max=50"`
Email string `json:"email" validate:"required,email"`
Age int `json:"age" validate:"required,min=1,max=120"`
}
// UpdateUserRequest represents the request payload for updating a user
type UpdateUserRequest struct {
FirstName *string `json:"first_name,omitempty" validate:"omitempty,min=2,max=50"`
LastName *string `json:"last_name,omitempty" validate:"omitempty,min=2,max=50"`
Email *string `json:"email,omitempty" validate:"omitempty,email"`
Age *int `json:"age,omitempty" validate:"omitempty,min=1,max=120"`
}
// NewUser creates a new user with generated ID and timestamps
func NewUser(req CreateUserRequest) *User {
now := time.Now()
return &User{
ID: uuid.New().String(),
FirstName: req.FirstName,
LastName: req.LastName,
Email: req.Email,
Age: req.Age,
CreatedAt: now,
UpdatedAt: now,
}
}
// Update updates user fields from UpdateUserRequest
func (u *User) Update(req UpdateUserRequest) {
if req.FirstName != nil {
u.FirstName = *req.FirstName
}
if req.LastName != nil {
u.LastName = *req.LastName
}
if req.Email != nil {
u.Email = *req.Email
}
if req.Age != nil {
u.Age = *req.Age
}
u.UpdatedAt = time.Now()
}
internal/repository/repository.go
package repository
import (
"errors"
"sync"
"golang-app/internal/models"
)
var (
ErrUserNotFound = errors.New("user not found")
ErrUserAlreadyExists = errors.New("user already exists")
)
// UserRepository defines the interface for user data operations
type UserRepository interface {
Create(user *models.User) error
GetByID(id string) (*models.User, error)
GetAll() ([]*models.User, error)
Update(user *models.User) error
Delete(id string) error
ExistsByEmail(email string) bool
}
// MemoryRepository implements UserRepository using in-memory storage
type MemoryRepository struct {
users map[string]*models.User
mutex sync.RWMutex
}
// NewMemoryRepository creates a new in-memory repository
func NewMemoryRepository() UserRepository {
return &MemoryRepository{
users: make(map[string]*models.User),
mutex: sync.RWMutex{},
}
}
// Create adds a new user to the repository
func (r *MemoryRepository) Create(user *models.User) error {
r.mutex.Lock()
defer r.mutex.Unlock()
if r.existsByEmail(user.Email) {
return ErrUserAlreadyExists
}
r.users[user.ID] = user
return nil
}
// GetByID retrieves a user by ID
func (r *MemoryRepository) GetByID(id string) (*models.User, error) {
r.mutex.RLock()
defer r.mutex.RUnlock()
user, exists := r.users[id]
if !exists {
return nil, ErrUserNotFound
}
// Return a copy to prevent external modifications
userCopy := *user
return &userCopy, nil
}
// GetAll retrieves all users
func (r *MemoryRepository) GetAll() ([]*models.User, error) {
r.mutex.RLock()
defer r.mutex.RUnlock()
users := make([]*models.User, 0, len(r.users))
for _, user := range r.users {
userCopy := *user
users = append(users, &userCopy)
}
return users, nil
}
// Update updates an existing user
func (r *MemoryRepository) Update(user *models.User) error {
r.mutex.Lock()
defer r.mutex.Unlock()
if _, exists := r.users[user.ID]; !exists {
return ErrUserNotFound
}
r.users[user.ID] = user
return nil
}
// Delete removes a user from the repository
func (r *MemoryRepository) Delete(id string) error {
r.mutex.Lock()
defer r.mutex.Unlock()
if _, exists := r.users[id]; !exists {
return ErrUserNotFound
}
delete(r.users, id)
return nil
}
// ExistsByEmail checks if a user with the given email exists
func (r *MemoryRepository) ExistsByEmail(email string) bool {
r.mutex.RLock()
defer r.mutex.RUnlock()
return r.existsByEmail(email)
}
// existsByEmail is the internal method without locking
func (r *MemoryRepository) existsByEmail(email string) bool {
for _, user := range r.users {
if user.Email == email {
return true
}
}
return false
}
internal/service/user_service.go
package service
import (
"errors"
"golang-app/internal/models"
"golang-app/internal/repository"
)
var (
ErrInvalidUserData = errors.New("invalid user data")
ErrDuplicateEmail = errors.New("email already exists")
)
// UserService defines the interface for user business logic
type UserService interface {
CreateUser(req models.CreateUserRequest) (*models.User, error)
GetUser(id string) (*models.User, error)
GetAllUsers() ([]*models.User, error)
UpdateUser(id string, req models.UpdateUserRequest) (*models.User, error)
DeleteUser(id string) error
}
// Service implements UserService
type Service struct {
repo repository.UserRepository
}
// New creates a new service instance
func New(repo repository.UserRepository) UserService {
return &Service{
repo: repo,
}
}
// CreateUser creates a new user
func (s *Service) CreateUser(req models.CreateUserRequest) (*models.User, error) {
// Check if user with email already exists
if s.repo.ExistsByEmail(req.Email) {
return nil, ErrDuplicateEmail
}
// Create new user
user := models.NewUser(req)
// Save to repository
if err := s.repo.Create(user); err != nil {
return nil, err
}
return user, nil
}
// GetUser retrieves a user by ID
func (s *Service) GetUser(id string) (*models.User, error) {
if id == "" {
return nil, ErrInvalidUserData
}
return s.repo.GetByID(id)
}
// GetAllUsers retrieves all users
func (s *Service) GetAllUsers() ([]*models.User, error) {
return s.repo.GetAll()
}
// UpdateUser updates an existing user
func (s *Service) UpdateUser(id string, req models.UpdateUserRequest) (*models.User, error) {
if id == "" {
return nil, ErrInvalidUserData
}
// Get existing user
user, err := s.repo.GetByID(id)
if err != nil {
return nil, err
}
// Check email uniqueness if email is being updated
if req.Email != nil && *req.Email != user.Email {
if s.repo.ExistsByEmail(*req.Email) {
return nil, ErrDuplicateEmail
}
}
// Update user
user.Update(req)
// Save changes
if err := s.repo.Update(user); err != nil {
return nil, err
}
return user, nil
}
// DeleteUser deletes a user
func (s *Service) DeleteUser(id string) error {
if id == "" {
return ErrInvalidUserData
}
return s.repo.Delete(id)
}
internal/handlers/handlers.go
package handlers
import (
"net/http"
"golang-app/internal/service"
"golang-app/internal/models"
"golang-app/internal/repository"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
)
// Handler holds the service dependencies
type Handler struct {
service service.UserService
validator *validator.Validate
}
// New creates a new handler instance
func New(service service.UserService) *Handler {
return &Handler{
service: service,
validator: validator.New(),
}
}
// ErrorResponse represents an error response
type ErrorResponse struct {
Error string `json:"error"`
Message string `json:"message,omitempty"`
}
// SuccessResponse represents a success response
type SuccessResponse struct {
Data interface{} `json:"data"`
Message string `json:"message,omitempty"`
}
// HealthCheck handles health check requests
func (h *Handler) HealthCheck(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": "healthy",
"service": "golang-app",
"version": "1.0.0",
})
}
// CreateUser handles user creation
func (h *Handler) CreateUser(c *gin.Context) {
var req models.CreateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{
Error: "invalid_request",
Message: err.Error(),
})
return
}
if err := h.validator.Struct(req); err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{
Error: "validation_error",
Message: err.Error(),
})
return
}
user, err := h.service.CreateUser(req)
if err != nil {
switch err {
case service.ErrDuplicateEmail:
c.JSON(http.StatusConflict, ErrorResponse{
Error: "duplicate_email",
Message: "User with this email already exists",
})
default:
c.JSON(http.StatusInternalServerError, ErrorResponse{
Error: "internal_error",
Message: "Failed to create user",
})
}
return
}
c.JSON(http.StatusCreated, SuccessResponse{
Data: user,
Message: "User created successfully",
})
}
// GetUser handles getting a single user
func (h *Handler) GetUser(c *gin.Context) {
id := c.Param("id")
user, err := h.service.GetUser(id)
if err != nil {
switch err {
case repository.ErrUserNotFound:
c.JSON(http.StatusNotFound, ErrorResponse{
Error: "user_not_found",
Message: "User not found",
})
default:
c.JSON(http.StatusInternalServerError, ErrorResponse{
Error: "internal_error",
Message: "Failed to get user",
})
}
return
}
c.JSON(http.StatusOK, SuccessResponse{
Data: user,
})
}
// GetUsers handles getting all users
func (h *Handler) GetUsers(c *gin.Context) {
users, err := h.service.GetAllUsers()
if err != nil {
c.JSON(http.StatusInternalServerError, ErrorResponse{
Error: "internal_error",
Message: "Failed to get users",
})
return
}
c.JSON(http.StatusOK, SuccessResponse{
Data: users,
})
}
// UpdateUser handles user updates
func (h *Handler) UpdateUser(c *gin.Context) {
id := c.Param("id")
var req models.UpdateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{
Error: "invalid_request",
Message: err.Error(),
})
return
}
if err := h.validator.Struct(req); err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{
Error: "validation_error",
Message: err.Error(),
})
return
}
user, err := h.service.UpdateUser(id, req)
if err != nil {
switch err {
case repository.ErrUserNotFound:
c.JSON(http.StatusNotFound, ErrorResponse{
Error: "user_not_found",
Message: "User not found",
})
case service.ErrDuplicateEmail:
c.JSON(http.StatusConflict, ErrorResponse{
Error: "duplicate_email",
Message: "User with this email already exists",
})
default:
c.JSON(http.StatusInternalServerError, ErrorResponse{
Error: "internal_error",
Message: "Failed to update user",
})
}
return
}
c.JSON(http.StatusOK, SuccessResponse{
Data: user,
Message: "User updated successfully",
})
}
// DeleteUser handles user deletion
func (h *Handler) DeleteUser(c *gin.Context) {
id := c.Param("id")
err := h.service.DeleteUser(id)
if err != nil {
switch err {
case repository.ErrUserNotFound:
c.JSON(http.StatusNotFound, ErrorResponse{
Error: "user_not_found",
Message: "User not found",
})
default:
c.JSON(http.StatusInternalServerError, ErrorResponse{
Error: "internal_error",
Message: "Failed to delete user",
})
}
return
}
c.JSON(http.StatusOK, SuccessResponse{
Message: "User deleted successfully",
})
}
internal/middleware/middleware.go
package middleware
import (
"fmt"
"time"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
// CORS middleware for handling Cross-Origin Resource Sharing
func CORS() gin.HandlerFunc {
return cors.New(cors.Config{
AllowOrigins: []string{"*"},
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowHeaders: []string{"*"},
ExposeHeaders: []string{"Content-Length"},
AllowCredentials: true,
MaxAge: 12 * time.Hour,
})
}
// RequestID middleware adds a unique request ID to each request
func RequestID() gin.HandlerFunc {
return func(c *gin.Context) {
requestID := c.GetHeader("X-Request-ID")
if requestID == "" {
requestID = uuid.New().String()
}
c.Header("X-Request-ID", requestID)
c.Set("RequestID", requestID)
c.Next()
}
}
// Logger middleware for structured logging
func Logger() gin.HandlerFunc {
return gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
return fmt.Sprintf("%s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n",
param.ClientIP,
param.TimeStamp.Format(time.RFC1123),
param.Method,
param.Path,
param.Request.Proto,
param.StatusCode,
param.Latency,
param.Request.UserAgent(),
param.ErrorMessage,
)
})
}
go.mod
module golang-app
go 1.23
require (
github.com/gin-contrib/cors v1.7.0
github.com/gin-gonic/gin v1.10.0
github.com/go-playground/validator/v10 v10.23.0
github.com/google/uuid v1.6.0
)
require (
github.com/bytedance/sonic v1.12.5 // indirect
github.com/bytedance/sonic/loader v0.2.1 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.7 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/goccy/go-json v0.10.4 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.12.0 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
google.golang.org/protobuf v1.36.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
Dockerfile
# Build stage
FROM golang:1.23-alpine AS builder
# Install build dependencies
RUN apk add --no-cache git ca-certificates tzdata
# Set working directory
WORKDIR /app
# Copy go mod files
COPY go.mod go.sum ./
# Download dependencies
RUN go mod download
# Copy source code
COPY . .
# Build the application
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .
# Final stage
FROM alpine:latest
# Install ca-certificates for HTTPS requests
RUN apk --no-cache add ca-certificates
# Set working directory
WORKDIR /root/
# Copy the binary from builder stage
COPY --from=builder /app/main .
# Expose port
EXPOSE 8080
# Set environment variables
ENV GIN_MODE=release
ENV PORT=8080
# Run the application
CMD ["./main"]
docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "8080:8080"
environment:
- PORT=8080
- ENVIRONMENT=development
- LOG_LEVEL=info
- DB_HOST=postgres
- DB_PORT=5432
- DB_USERNAME=golang_user
- DB_PASSWORD=golang_pass
- DB_DATABASE=golang_app
- DB_SSL_MODE=disable
depends_on:
- postgres
networks:
- app-network
postgres:
image: postgres:16-alpine
environment:
- POSTGRES_USER=golang_user
- POSTGRES_PASSWORD=golang_pass
- POSTGRES_DB=golang_app
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- app-network
redis:
image: redis:7-alpine
ports:
- "6379:6379"
networks:
- app-network
networks:
app-network:
driver: bridge
volumes:
postgres_data:
Makefile
# Variables
APP_NAME=golang-app
BINARY_NAME=main
DOCKER_IMAGE=$(APP_NAME)
VERSION=1.0.0
# Go parameters
GOCMD=go
GOBUILD=$(GOCMD) build
GOCLEAN=$(GOCMD) clean
GOTEST=$(GOCMD) test
GOGET=$(GOCMD) get
GOMOD=$(GOCMD) mod
GOFMT=gofmt
.PHONY: all build clean test deps fmt vet run docker-build docker-run help
# Default target
all: clean deps fmt vet test build
# Build the application
build:
@echo "Building $(APP_NAME)..."
$(GOBUILD) -o $(BINARY_NAME) -v ./...
# Clean build files
clean:
@echo "Cleaning..."
$(GOCLEAN)
rm -f $(BINARY_NAME)
# Run tests
test:
@echo "Running tests..."
$(GOTEST) -v ./...
# Run tests with coverage
test-coverage:
@echo "Running tests with coverage..."
$(GOTEST) -v -coverprofile=coverage.out ./...
$(GOCMD) tool cover -html=coverage.out -o coverage.html
# Download dependencies
deps:
@echo "Downloading dependencies..."
$(GOMOD) download
$(GOMOD) tidy
# Format code
fmt:
@echo "Formatting code..."
$(GOFMT) -s -w .
# Run go vet
vet:
@echo "Running go vet..."
$(GOCMD) vet ./...
# Run the application
run:
@echo "Running $(APP_NAME)..."
$(GOBUILD) -o $(BINARY_NAME) -v ./...
./$(BINARY_NAME)
# Run with hot reload (requires air: go install github.com/cosmtrek/air@latest)
dev:
@echo "Running with hot reload..."
air
# Build Docker image
docker-build:
@echo "Building Docker image..."
docker build -t $(DOCKER_IMAGE):$(VERSION) .
docker tag $(DOCKER_IMAGE):$(VERSION) $(DOCKER_IMAGE):latest
# Run Docker container
docker-run:
@echo "Running Docker container..."
docker run -p 8080:8080 --env-file .env $(DOCKER_IMAGE):latest
# Run with docker-compose
docker-compose-up:
@echo "Starting services with docker-compose..."
docker-compose up --build
# Stop docker-compose services
docker-compose-down:
@echo "Stopping docker-compose services..."
docker-compose down
# Lint code (requires golangci-lint)
lint:
@echo "Running linter..."
golangci-lint run ./...
# Security scan (requires gosec)
security:
@echo "Running security scan..."
gosec ./...
# Install development tools
install-tools:
@echo "Installing development tools..."
go install github.com/cosmtrek/air@latest
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
go install github.com/securecodewarrior/gosec/v2/cmd/gosec@latest
# Generate API documentation (requires swag)
docs:
@echo "Generating API documentation..."
swag init
# Help
help:
@echo "Available commands:"
@echo " build - Build the application"
@echo " clean - Clean build files"
@echo " test - Run tests"
@echo " test-coverage - Run tests with coverage"
@echo " deps - Download dependencies"
@echo " fmt - Format code"
@echo " vet - Run go vet"
@echo " run - Run the application"
@echo " dev - Run with hot reload"
@echo " docker-build - Build Docker image"
@echo " docker-run - Run Docker container"
@echo " docker-compose-up - Start with docker-compose"
@echo " docker-compose-down- Stop docker-compose"
@echo " lint - Run linter"
@echo " security - Run security scan"
@echo " install-tools - Install development tools"
@echo " docs - Generate API documentation"
@echo " help - Show this help"
.env.example
# Application Configuration
PORT=8080
ENVIRONMENT=development
LOG_LEVEL=info
# Database Configuration
DB_HOST=localhost
DB_PORT=5432
DB_USERNAME=golang_user
DB_PASSWORD=golang_pass
DB_DATABASE=golang_app
DB_SSL_MODE=disable
# Redis Configuration (if needed)
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=
# JWT Configuration (if auth is implemented)
JWT_SECRET=your-secret-key
JWT_EXPIRES_IN=24h
# External APIs (if needed)
API_BASE_URL=https://api.example.com
API_KEY=your-api-key
README.md
A production-ready REST API template built with Go, featuring clean architecture, comprehensive middleware, and Docker support.
## Features
- **Clean Architecture**: Separation of concerns with handlers, services, and repository layers
- **RESTful API**: Complete CRUD operations for user management
- **Middleware**: CORS, Request ID, logging, and recovery middleware
- **Configuration**: Environment-based configuration management
- **Validation**: Request validation using struct tags
- **Error Handling**: Structured error responses
- **Docker Support**: Multi-stage Dockerfile and docker-compose setup
- **Database Ready**: PostgreSQL and Redis integration configured
- **Production Ready**: Graceful shutdown, timeouts, and proper logging
## Quick Start
### Prerequisites
- Go 1.23 or higher
- Docker and Docker Compose (optional)
- PostgreSQL (if not using Docker)
### Local Development
1. Clone the repository:
```bash
git clone <repository-url>
cd golang-app
```
2. Install dependencies:
```bash
make deps
```
3. Copy environment file:
```bash
cp .env.example .env
```
4. Run the application:
```bash
make run
```
The API will be available at `http://localhost:8080`
### Using Docker
1. Build and run with docker-compose:
```bash
make docker-compose-up
```
2. The API will be available at `http://localhost:8080`
## API Endpoints
### Health Check
- `GET /health` - Health check endpoint
### User Management
- `GET /api/v1/users` - Get all users
- `POST /api/v1/users` - Create a new user
- `GET /api/v1/users/:id` - Get user by ID
- `PUT /api/v1/users/:id` - Update user
- `DELETE /api/v1/users/:id` - Delete user
## Environment Variables
| Variable | Description | Default |
|----------|-------------|---------|
| `PORT` | Server port | `8080` |
| `ENVIRONMENT` | Environment (development/production) | `development` |
| `LOG_LEVEL` | Log level (debug/info/warn/error) | `info` |
| `DB_HOST` | Database host | `localhost` |
| `DB_PORT` | Database port | `5432` |
| `DB_USERNAME` | Database username | `postgres` |
| `DB_PASSWORD` | Database password | `` |
| `DB_DATABASE` | Database name | `golang_app` |
| `DB_SSL_MODE` | Database SSL mode | `disable` |
## Development Commands
```bash
# Install dependencies
make deps
# Format code
make fmt
# Run tests
make test
# Run tests with coverage
make test-coverage
# Build application
make build
# Run application
make run
# Run with hot reload
make dev
# Run linter
make lint
# Run security scan
make security
# Install development tools
make install-tools
# Build Docker image
make docker-build
# Run with docker-compose
make docker-compose-up
```
## Testing
Run tests with:
```bash
make test
```
Generate coverage report:
```bash
make test-coverage
```
## Contributing
1. Fork the repository
2. Create a feature branch: `git checkout -b feature-name`
3. Make changes and add tests
4. Run tests: `make test`
5. Format code: `make fmt`
6. Commit changes: `git commit -am 'Add feature'`
7. Push to branch: `git push origin feature-name`
8. Submit a pull request
## License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
## Architecture
This template follows clean architecture principles:
- **Handlers**: Handle HTTP requests and responses
- **Services**: Contain business logic
- **Repository**: Handle data persistence
- **Models**: Define data structures
- **Middleware**: Handle cross-cutting concerns
## Deployment
### Docker
Build and run with Docker:
```bash
docker build -t golang-app .
docker run -p 8080:8080 golang-app
```
.gitignore
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
main
golang-app
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
coverage.html
coverage.out
# Dependency directories (remove the comment below to include it)
vendor/
# Go workspace file
go.work
# Environment files
.env
.env.local
.env.development
.env.test
.env.production
# IDE files
.vscode/
.idea/
*.swp
*.swo
*~
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Logs
*.log
logs/
# Runtime data
pids
*.pid
*.seed
# Coverage directory used by tools like istanbul
coverage/
# Temporary directories
tmp/
temp/
# Build directories
build/
dist/
# Docker
.dockerignore
# Air (hot reload) temporary files
tmp/
# Database files
*.db
*.sqlite
*.sqlite3
# Backup files
*.backup
*.bak
# Certificate files
*.pem
*.key
*.crt
.air.toml
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"
[build]
args_bin = []
bin = "./tmp/main"
cmd = "go build -o ./tmp/main ."
delay = 0
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html"]
include_file = []
kill_delay = "0s"
log = "build-errors.log"
poll = false
poll_interval = 0
rerun = false
rerun_delay = 500
send_interrupt = false
stop_on_root = false
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
main_only = false
time = false
[misc]
clean_on_exit = false
[screen]
clear_on_rebuild = false
keep_scroll = true
scripts/build.sh
#!/bin/bash
set -e
# Build script for Golang Web API
APP_NAME="golang-app"
VERSION=${VERSION:-"1.0.0"}
BUILD_DIR="build"
BINARY_NAME="main"
echo "Building $APP_NAME v$VERSION..."
# Create build directory
mkdir -p $BUILD_DIR
# Get build info
BUILD_TIME=$(date -u '+%Y-%m-%d_%I:%M:%S%p')
GIT_COMMIT=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
GO_VERSION=$(go version | awk '{print $3}')
# Build flags
LDFLAGS="-X main.Version=$VERSION -X main.BuildTime=$BUILD_TIME -X main.GitCommit=$GIT_COMMIT -X main.GoVersion=$GO_VERSION"
# Build for current platform
echo "Building for current platform..."
CGO_ENABLED=0 go build -ldflags "$LDFLAGS" -o $BUILD_DIR/$BINARY_NAME
# Build for multiple platforms if requested
if [ "$1" = "cross" ]; then
echo "Cross-compiling for multiple platforms..."
# Linux AMD64
echo "Building for Linux AMD64..."
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags "$LDFLAGS" -o $BUILD_DIR/${BINARY_NAME}-linux-amd64
# Linux ARM64
echo "Building for Linux ARM64..."
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -ldflags "$LDFLAGS" -o $BUILD_DIR/${BINARY_NAME}-linux-arm64
# macOS AMD64
echo "Building for macOS AMD64..."
GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build -ldflags "$LDFLAGS" -o $BUILD_DIR/${BINARY_NAME}-darwin-amd64
# macOS ARM64
echo "Building for macOS ARM64..."
GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 go build -ldflags "$LDFLAGS" -o $BUILD_DIR/${BINARY_NAME}-darwin-arm64
# Windows AMD64
echo "Building for Windows AMD64..."
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -ldflags "$LDFLAGS" -o $BUILD_DIR/${BINARY_NAME}-windows-amd64.exe
fi
echo "Build complete! Binaries are in the $BUILD_DIR directory."
scripts/deploy.sh
#!/bin/bash
set -e
# Deployment script for Golang Web API
APP_NAME="golang-app"
VERSION=${VERSION:-"latest"}
REGISTRY=${REGISTRY:-"your-registry.com"}
ENVIRONMENT=${ENVIRONMENT:-"staging"}
echo "Deploying $APP_NAME:$VERSION to $ENVIRONMENT..."
# Build Docker image
echo "Building Docker image..."
docker build -t $REGISTRY/$APP_NAME:$VERSION .
docker tag $REGISTRY/$APP_NAME:$VERSION $REGISTRY/$APP_NAME:latest
# Push to registry
echo "Pushing to registry..."
docker push $REGISTRY/$APP_NAME:$VERSION
docker push $REGISTRY/$APP_NAME:latest
# Deploy based on environment
case $ENVIRONMENT in
"development"|"dev")
echo "Deploying to development environment..."
docker-compose -f docker-compose.dev.yml up -d
;;
"staging")
echo "Deploying to staging environment..."
kubectl apply -f k8s/staging/
kubectl set image deployment/$APP_NAME $APP_NAME=$REGISTRY/$APP_NAME:$VERSION -n staging
;;
"production"|"prod")
echo "Deploying to production environment..."
kubectl apply -f k8s/production/
kubectl set image deployment/$APP_NAME $APP_NAME=$REGISTRY/$APP_NAME:$VERSION -n production
;;
*)
echo "Unknown environment: $ENVIRONMENT"
exit 1
;;
esac
echo "Deployment complete!"
internal/handlers/handlers_test.go
package handlers
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"golang-app/internal/models"
"golang-app/internal/service"
)
// MockUserService is a mock implementation of UserService
type MockUserService struct {
mock.Mock
}
func (m *MockUserService) CreateUser(req models.CreateUserRequest) (*models.User, error) {
args := m.Called(req)
return args.Get(0).(*models.User), args.Error(1)
}
func (m *MockUserService) GetUser(id string) (*models.User, error) {
args := m.Called(id)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*models.User), args.Error(1)
}
func (m *MockUserService) GetAllUsers() ([]*models.User, error) {
args := m.Called()
return args.Get(0).([]*models.User), args.Error(1)
}
func (m *MockUserService) UpdateUser(id string, req models.UpdateUserRequest) (*models.User, error) {
args := m.Called(id, req)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*models.User), args.Error(1)
}
func (m *MockUserService) DeleteUser(id string) error {
args := m.Called(id)
return args.Error(0)
}
func setupRouter() *gin.Engine {
gin.SetMode(gin.TestMode)
return gin.New()
}
func TestHealthCheck(t *testing.T) {
router := setupRouter()
mockService := new(MockUserService)
handler := New(mockService)
router.GET("/health", handler.HealthCheck)
req, _ := http.NewRequest("GET", "/health", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var response map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Equal(t, "healthy", response["status"])
assert.Equal(t, "golang-app", response["service"])
}
func TestCreateUser(t *testing.T) {
router := setupRouter()
mockService := new(MockUserService)
handler := New(mockService)
router.POST("/users", handler.CreateUser)
// Test successful creation
user := &models.User{
ID: "123",
FirstName: "John",
LastName: "Doe",
Email: "john.doe@example.com",
Age: 25,
}
mockService.On("CreateUser", mock.AnythingOfType("models.CreateUserRequest")).Return(user, nil)
reqBody := models.CreateUserRequest{
FirstName: "John",
LastName: "Doe",
Email: "john.doe@example.com",
Age: 25,
}
jsonBody, _ := json.Marshal(reqBody)
req, _ := http.NewRequest("POST", "/users", bytes.NewBuffer(jsonBody))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusCreated, w.Code)
mockService.AssertExpectations(t)
}
func TestGetUser(t *testing.T) {
router := setupRouter()
mockService := new(MockUserService)
handler := New(mockService)
router.GET("/users/:id", handler.GetUser)
// Test successful retrieval
user := &models.User{
ID: "123",
FirstName: "John",
LastName: "Doe",
Email: "john.doe@example.com",
Age: 25,
}
mockService.On("GetUser", "123").Return(user, nil)
req, _ := http.NewRequest("GET", "/users/123", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
mockService.AssertExpectations(t)
}
internal/service/user_service_test.go
package service
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"golang-app/internal/models"
"golang-app/internal/repository"
)
// MockUserRepository is a mock implementation of UserRepository
type MockUserRepository struct {
mock.Mock
}
func (m *MockUserRepository) Create(user *models.User) error {
args := m.Called(user)
return args.Error(0)
}
func (m *MockUserRepository) GetByID(id string) (*models.User, error) {
args := m.Called(id)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*models.User), args.Error(1)
}
func (m *MockUserRepository) GetAll() ([]*models.User, error) {
args := m.Called()
return args.Get(0).([]*models.User), args.Error(1)
}
func (m *MockUserRepository) Update(user *models.User) error {
args := m.Called(user)
return args.Error(0)
}
func (m *MockUserRepository) Delete(id string) error {
args := m.Called(id)
return args.Error(0)
}
func (m *MockUserRepository) ExistsByEmail(email string) bool {
args := m.Called(email)
return args.Bool(0)
}
func TestCreateUser(t *testing.T) {
mockRepo := new(MockUserRepository)
service := New(mockRepo)
req := models.CreateUserRequest{
FirstName: "John",
LastName: "Doe",
Email: "john.doe@example.com",
Age: 25,
}
// Test successful creation
mockRepo.On("ExistsByEmail", req.Email).Return(false)
mockRepo.On("Create", mock.AnythingOfType("*models.User")).Return(nil)
user, err := service.CreateUser(req)
assert.NoError(t, err)
assert.NotNil(t, user)
assert.Equal(t, req.FirstName, user.FirstName)
assert.Equal(t, req.Email, user.Email)
mockRepo.AssertExpectations(t)
}
func TestCreateUser_DuplicateEmail(t *testing.T) {
mockRepo := new(MockUserRepository)
service := New(mockRepo)
req := models.CreateUserRequest{
FirstName: "John",
LastName: "Doe",
Email: "john.doe@example.com",
Age: 25,
}
// Test duplicate email
mockRepo.On("ExistsByEmail", req.Email).Return(true)
user, err := service.CreateUser(req)
assert.Error(t, err)
assert.Nil(t, user)
assert.Equal(t, ErrDuplicateEmail, err)
mockRepo.AssertExpectations(t)
}
func TestGetUser(t *testing.T) {
mockRepo := new(MockUserRepository)
service := New(mockRepo)
expectedUser := &models.User{
ID: "123",
FirstName: "John",
LastName: "Doe",
Email: "john.doe@example.com",
Age: 25,
}
// Test successful retrieval
mockRepo.On("GetByID", "123").Return(expectedUser, nil)
user, err := service.GetUser("123")
assert.NoError(t, err)
assert.NotNil(t, user)
assert.Equal(t, expectedUser.ID, user.ID)
assert.Equal(t, expectedUser.Email, user.Email)
mockRepo.AssertExpectations(t)
}
func TestGetUser_NotFound(t *testing.T) {
mockRepo := new(MockUserRepository)
service := New(mockRepo)
// Test user not found
mockRepo.On("GetByID", "nonexistent").Return(nil, repository.ErrUserNotFound)
user, err := service.GetUser("nonexistent")
assert.Error(t, err)
assert.Nil(t, user)
assert.Equal(t, repository.ErrUserNotFound, err)
mockRepo.AssertExpectations(t)
}
k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: golang-app
labels:
app: golang-app
spec:
replicas: 3
selector:
matchLabels:
app: golang-app
template:
metadata:
labels:
app: golang-app
spec:
containers:
- name: golang-app
image: golang-app:latest
ports:
- containerPort: 8080
env:
- name: PORT
value: "8080"
- name: ENVIRONMENT
value: "production"
- name: LOG_LEVEL
value: "info"
- name: DB_HOST
valueFrom:
secretKeyRef:
name: app-secrets
key: db-host
- name: DB_PORT
value: "5432"
- name: DB_USERNAME
valueFrom:
secretKeyRef:
name: app-secrets
key: db-username
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: app-secrets
key: db-password
- name: DB_DATABASE
valueFrom:
secretKeyRef:
name: app-secrets
key: db-database
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
resources:
requests:
memory: "64Mi"
cpu: "250m"
limits:
memory: "128Mi"
cpu: "500m"
---
apiVersion: v1
kind: Service
metadata:
name: golang-app-service
spec:
selector:
app: golang-app
ports:
- protocol: TCP
port: 80
targetPort: 8080
type: LoadBalancer
---
apiVersion: v1
kind: Secret
metadata:
name: app-secrets
type: Opaque
stringData:
db-host: "postgres-service"
db-username: "golang_user"
db-password: "your-secret-password"
db-database: "golang_app"
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: golang-app-ingress
annotations:
kubernetes.io/ingress.class: "nginx"
cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
tls:
- hosts:
- api.yourdomain.com
secretName: golang-app-tls
rules:
- host: api.yourdomain.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: golang-app-service
port:
number: 80
.github/workflows/ci-cd.yml
name: CI/CD Pipeline
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
env:
GO_VERSION: 1.23
DOCKER_IMAGE: golang-app
jobs:
test:
name: Test
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: ${{ env.GO_VERSION }}
- name: Cache Go modules
uses: actions/cache@v3
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Install dependencies
run: go mod download
- name: Run tests
run: go test -v -race -coverprofile=coverage.out ./...
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage.out
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: ${{ env.GO_VERSION }}
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v3
with:
version: latest
security:
name: Security Scan
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run Gosec Security Scanner
uses: securecodewarrior/github-action-gosec@master
with:
args: './...'
build:
name: Build and Push Docker Image
runs-on: ubuntu-latest
needs: [test, lint, security]
if: github.ref == 'refs/heads/main'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.DOCKER_IMAGE }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=sha,prefix={{branch}}-
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
deploy:
name: Deploy to Production
runs-on: ubuntu-latest
needs: [build]
if: github.ref == 'refs/heads/main'
environment: production
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Deploy to Kubernetes
run: |
# Add your deployment script here
echo "Deploying to production..."
# kubectl apply -f k8s/
api-spec.json
{
"openapi": "3.0.0",
"info": {
"title": "Golang Web API",
"description": "A production-ready REST API built with Go",
"version": "1.0.0",
"contact": {
"name": "API Support",
"email": "support@example.com"
},
"license": {
"name": "MIT",
"url": "https://opensource.org/licenses/MIT"
}
},
"servers": [
{
"url": "http://localhost:8080",
"description": "Development server"
},
{
"url": "https://api.yourdomain.com",
"description": "Production server"
}
],
"paths": {
"/health": {
"get": {
"summary": "Health check endpoint",
"tags": ["Health"],
"responses": {
"200": {
"description": "Service is healthy",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": {
"type": "string",
"example": "healthy"
},
"service": {
"type": "string",
"example": "golang-app"
},
"version": {
"type": "string",
"example": "1.0.0"
}
}
}
}
}
}
}
}
},
"/api/v1/users": {
"get": {
"summary": "Get all users",
"tags": ["Users"],
"responses": {
"200": {
"description": "List of users",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/User"
}
}
}
}
}
}
}
}
},
"post": {
"summary": "Create a new user",
"tags": ["Users"],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateUserRequest"
}
}
}
},
"responses": {
"201": {
"description": "User created successfully",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"$ref": "#/components/schemas/User"
},
"message": {
"type": "string",
"example": "User created successfully"
}
}
}
}
}
},
"400": {
"description": "Invalid request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
},
"409": {
"description": "Email already exists",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
}
}
},
"/api/v1/users/{id}": {
"get": {
"summary": "Get user by ID",
"tags": ["Users"],
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "User details",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"$ref": "#/components/schemas/User"
}
}
}
}
}
},
"404": {
"description": "User not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
}
},
"put": {
"summary": "Update user",
"tags": ["Users"],
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UpdateUserRequest"
}
}
}
},
"responses": {
"200": {
"description": "User updated successfully",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"$ref": "#/components/schemas/User"
},
"message": {
"type": "string",
"example": "User updated successfully"
}
}
}
}
}
},
"404": {
"description": "User not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
}
},
"delete": {
"summary": "Delete user",
"tags": ["Users"],
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "User deleted successfully",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"message": {
"type": "string",
"example": "User deleted successfully"
}
}
}
}
}
},
"404": {
"description": "User not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"User": {
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid",
"example": "550e8400-e29b-41d4-a716-446655440000"
},
"first_name": {
"type": "string",
"example": "John"
},
"last_name": {
"type": "string",
"example": "Doe"
},
"email": {
"type": "string",
"format": "email",
"example": "john.doe@example.com"
},
"age": {
"type": "integer",
"minimum": 1,
"maximum": 120,
"example": 25
},
"created_at": {
"type": "string",
"format": "date-time",
"example": "2025-01-01T00:00:00Z"
},
"updated_at": {
"type": "string",
"format": "date-time",
"example": "2025-01-01T00:00:00Z"
}
},
"required": ["id", "first_name", "last_name", "email", "age", "created_at", "updated_at"]
},
"CreateUserRequest": {
"type": "object",
"properties": {
"first_name": {
"type": "string",
"minLength": 2,
"maxLength": 50,
"example": "John"
},
"last_name": {
"type": "string",
"minLength": 2,
"maxLength": 50,
"example": "Doe"
},
"email": {
"type": "string",
"format": "email",
"example": "john.doe@example.com"
},
"age": {
"type": "integer",
"minimum": 1,
"maximum": 120,
"example": 25
}
},
"required": ["first_name", "last_name", "email", "age"]
},
"UpdateUserRequest": {
"type": "object",
"properties": {
"first_name": {
"type": "string",
"minLength": 2,
"maxLength": 50,
"example": "John"
},
"last_name": {
"type": "string",
"minLength": 2,
"maxLength": 50,
"example": "Doe"
},
"email": {
"type": "string",
"format": "email",
"example": "john.doe@example.com"
},
"age": {
"type": "integer",
"minimum": 1,
"maximum": 120,
"example": 25
}
}
},
"ErrorResponse": {
"type": "object",
"properties": {
"error": {
"type": "string",
"example": "validation_error"
},
"message": {
"type": "string",
"example": "Invalid request data"
}
},
"required": ["error"]
}
}
}
}
Common Button Mistake in UI/UX
Next
Want to update?
Subscribe to my blog to receive the latest updates from us.
Monthly articles
New articles will be created every month. No spam
All notifications are notified to you, not spam or advertising.