new file: .env

modified:   Dockerfile
	new file:   cmd/api/main.go
	modified:   docker-compose.yaml
	modified:   go.mod
	modified:   go.sum
	new file:   internal/config/config.go
	new file:   internal/handler/middleware.go
	new file:   internal/handler/user_handler.go
	new file:   internal/model/user.go
	new file:   internal/repository/user_repository.go
	new file:   internal/server/server.go
	new file:   internal/service/user_service.go
	new file:   main
	deleted:    main.go
	new file:   migrations/001_create_users.sql
	new file:   pkg/database/postgres.go
	deleted:    src/models/user.go
add files from deepseek for rest api server on golang gorm and chi
server router
This commit is contained in:
2025-09-27 04:44:46 +05:00
parent 12c855cacc
commit 727c94d0a9
18 changed files with 508 additions and 98 deletions
+7
View File
@@ -0,0 +1,7 @@
# DB environment variabels
DB_HOST=localhost
DB_PORT=5432
DB_USER=postgres
DB_PASSWORD=postgres
DB_NAME=mydb
APP_PORT=8080
+28 -9
View File
@@ -1,14 +1,33 @@
# Build stage # Билд стадия
FROM golang:1.25.1-alpine as builder FROM golang:1.25.1-alpine AS builder
WORKDIR /app WORKDIR /app
# Копируем зависимости
COPY go.mod go.sum ./ COPY go.mod go.sum ./
RUN go mod download RUN go mod download
COPY *.go ./
RUN CGO_ENABLED=0 GOOS=linux go build -o /go/bin/app
# Final stage # Копируем исходный код
FROM gcr.io/distroless/static-debian12 COPY . .
WORKDIR /
COPY --from=builder /go/bin/app /app # Собираем приложение
CMD ["/app"] RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main ./cmd/api
# Финальная стадия
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
# Копируем бинарник из builder стадии
COPY --from=builder /app/main .
# Копируем миграции
COPY --from=builder /app/migrations ./migrations
# Экспозим порт
EXPOSE 8080 EXPOSE 8080
# Запускаем приложение
CMD ["./main"]
+27
View File
@@ -0,0 +1,27 @@
package main
import (
"log"
"serv_golang_rest_api/internal/config"
"serv_golang_rest_api/internal/server"
"serv_golang_rest_api/pkg/database"
)
func main() {
// Загрузка конфигурации
cfg := config.Load()
// Подключение к БД
db, err := database.NewPostgresConnection(cfg)
if err != nil {
log.Fatal("Failed to connect to database:", err)
}
// Создание и запуск сервера
srv := server.New(db)
log.Printf("Server starting on port %s", cfg.AppPort)
if err := srv.Run(cfg.AppPort); err != nil {
log.Fatal("Failed to start server:", err)
}
}
+16 -2
View File
@@ -1,5 +1,5 @@
services: services:
go-server: api:
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
@@ -8,7 +8,8 @@ services:
container_name: serv_golang_rest_api container_name: serv_golang_rest_api
restart: unless-stopped restart: unless-stopped
depends_on: depends_on:
- db db:
condition: service_healthy
environment: environment:
# Database connection settings # Database connection settings
DB_HOST: db DB_HOST: db
@@ -16,8 +17,14 @@ services:
DB_USER: postgres DB_USER: postgres
DB_PASSWORD: postgres DB_PASSWORD: postgres
DB_NAME: mydb DB_NAME: mydb
APP_PORT: 8080
networks: networks:
- app-network - app-network
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
db: db:
image: postgres:15-alpine image: postgres:15-alpine
@@ -29,11 +36,18 @@ services:
POSTGRES_DB: mydb POSTGRES_DB: mydb
volumes: volumes:
- postgres_data:/var/lib/postgresql/data - postgres_data:/var/lib/postgresql/data
- ./migrations:/docker-entrypoint-initdb.d
networks: networks:
- app-network - app-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
volumes: volumes:
postgres_data: postgres_data:
networks: networks:
app-network: app-network:
driver: bridge
+5 -2
View File
@@ -1,6 +1,6 @@
module serv_golang_rest_api module serv_golang_rest_api
go 1.22.5 go 1.25.1
require ( require (
gorm.io/driver/postgres v1.6.0 gorm.io/driver/postgres v1.6.0
@@ -8,13 +8,16 @@ require (
) )
require ( require (
github.com/go-chi/chi/v5 v5.2.3
github.com/go-chi/cors v1.2.2
github.com/google/uuid v1.6.0
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.6.0 // indirect github.com/jackc/pgx/v5 v5.6.0 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
golang.org/x/crypto v0.31.0 // indirect golang.org/x/crypto v0.31.0
golang.org/x/sync v0.10.0 // indirect golang.org/x/sync v0.10.0 // indirect
golang.org/x/text v0.21.0 // indirect golang.org/x/text v0.21.0 // indirect
) )
+6
View File
@@ -1,6 +1,12 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
@@ -0,0 +1,30 @@
package config
import "os"
type Config struct {
DBHost string
DBPort string
DBUser string
DBPassword string
DBName string
AppPort string
}
func Load() *Config {
return &Config{
DBHost: getEnv("DB_HOST", "localhost"),
DBPort: getEnv("DB_PORT", "5432"),
DBUser: getEnv("DB_USER", "postgres"),
DBPassword: getEnv("DB_PASSWORD", "postgres"),
DBName: getEnv("DB_NAME", "mydb"),
AppPort: getEnv("APP_PORT", "8080"),
}
}
func getEnv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
@@ -0,0 +1,25 @@
package handler
import (
"net/http"
"time"
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/cors"
)
func CommonMiddleware() []func(http.Handler) http.Handler {
return []func(http.Handler) http.Handler{
middleware.Logger,
middleware.Recoverer,
middleware.Timeout(60 * time.Second),
cors.Handler(cors.Options{
AllowedOrigins: []string{"https://*", "http://*"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
ExposedHeaders: []string{"Link"},
AllowCredentials: false,
MaxAge: 300,
}),
}
}
@@ -0,0 +1,66 @@
package handler
import (
"encoding/json"
"net/http"
"strconv"
"github.com/go-chi/chi/v5"
"serv_golang_rest_api/internal/model"
"serv_golang_rest_api/internal/service"
)
type UserHandler struct {
userService *service.UserService
}
func NewUserHandler(userService *service.UserService) *UserHandler {
return &UserHandler{userService: userService}
}
func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
var req model.CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
user, err := h.userService.CreateUser(&req)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(user)
}
func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {
idStr := chi.URLParam(r, "id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
http.Error(w, "Invalid user ID", http.StatusBadRequest)
return
}
user, err := h.userService.GetUserByID(uint(id))
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}
func (h *UserHandler) GetAllUsers(w http.ResponseWriter, r *http.Request) {
users, err := h.userService.GetAllUsers()
if err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(users)
}
@@ -0,0 +1,37 @@
package model
import (
"time"
"gorm.io/gorm"
)
type User struct {
ID uint `json:"id" gorm:"primarykey"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `json:"deleted_at,omitempty" gorm:"index"`
Name string `json:"name" gorm:"size:100;not null"`
Email string `json:"email" gorm:"size:255;uniqueIndex;not null"`
Password string `json:"-" gorm:"size:255;not null"` // Пароль не возвращаем в JSON
}
type CreateUserRequest struct {
Name string `json:"name" validate:"required,min=2,max=100"`
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required,min=6"`
}
type UpdateUserRequest struct {
Name string `json:"name" validate:"omitempty,min=2,max=100"`
Email string `json:"email" validate:"omitempty,email"`
}
type UserResponse struct {
ID uint `json:"id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Name string `json:"name"`
Email string `json:"email"`
}
@@ -0,0 +1,45 @@
package repository
import (
"serv_golang_rest_api/internal/model"
"gorm.io/gorm"
)
type UserRepository struct {
db *gorm.DB
}
func NewUserRepository(db *gorm.DB) *UserRepository {
return &UserRepository{db: db}
}
func (r *UserRepository) Create(user *model.User) error {
return r.db.Create(user).Error
}
func (r *UserRepository) FindByID(id uint) (*model.User, error) {
var user model.User
err := r.db.First(&user, id).Error
return &user, err
}
func (r *UserRepository) FindByEmail(email string) (*model.User, error) {
var user model.User
err := r.db.Where("email = ?", email).First(&user).Error
return &user, err
}
func (r *UserRepository) FindAll() ([]model.User, error) {
var users []model.User
err := r.db.Find(&users).Error
return users, err
}
func (r *UserRepository) Update(user *model.User) error {
return r.db.Save(user).Error
}
func (r *UserRepository) Delete(id uint) error {
return r.db.Delete(&model.User{}, id).Error
}
@@ -0,0 +1,77 @@
package server
import (
"encoding/json"
"net/http"
"serv_golang_rest_api/internal/handler"
"serv_golang_rest_api/internal/repository"
"serv_golang_rest_api/internal/service"
"github.com/go-chi/chi/v5"
"gorm.io/gorm"
)
type Server struct {
router *chi.Mux
db *gorm.DB
}
func New(db *gorm.DB) *Server {
s := &Server{
router: chi.NewRouter(),
db: db,
}
s.configureRouter()
return s
}
func (s *Server) configureRouter() {
// Общие middleware
for _, middleware := range handler.CommonMiddleware() {
s.router.Use(middleware)
}
// Health check
s.router.Get("/health", s.healthCheck)
// API routes
s.router.Route("/api/v1", func(r chi.Router) {
s.setupUserRoutes(r)
})
}
func (s *Server) setupUserRoutes(r chi.Router) {
userRepo := repository.NewUserRepository(s.db)
userService := service.NewUserService(userRepo)
userHandler := handler.NewUserHandler(userService)
r.Route("/users", func(r chi.Router) {
r.Get("/", userHandler.GetAllUsers)
r.Post("/", userHandler.CreateUser)
r.Get("/{id}", userHandler.GetUser)
})
}
func (s *Server) healthCheck(w http.ResponseWriter, r *http.Request) {
// Проверяем соединение с БД
sqlDB, err := s.db.DB()
if err != nil {
http.Error(w, "Database connection error", http.StatusServiceUnavailable)
return
}
if err := sqlDB.Ping(); err != nil {
http.Error(w, "Database ping failed", http.StatusServiceUnavailable)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"status": "healthy",
"timestamp": http.TimeFormat,
})
}
func (s *Server) Run(port string) error {
return http.ListenAndServe(":"+port, s.router)
}
@@ -0,0 +1,76 @@
package service
import (
"errors"
"serv_golang_rest_api/internal/model"
"serv_golang_rest_api/internal/repository"
"golang.org/x/crypto/bcrypt"
)
type UserService struct {
userRepo *repository.UserRepository
}
func NewUserService(userRepo *repository.UserRepository) *UserService {
return &UserService{userRepo: userRepo}
}
func (s *UserService) CreateUser(req *model.CreateUserRequest) (*model.UserResponse, error) {
// Проверяем существует ли пользователь с таким email
existingUser, _ := s.userRepo.FindByEmail(req.Email)
if existingUser != nil {
return nil, errors.New("user with this email already exists")
}
// Хешируем пароль
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
return nil, err
}
user := &model.User{
Name: req.Name,
Email: req.Email,
Password: string(hashedPassword),
}
if err := s.userRepo.Create(user); err != nil {
return nil, err
}
return s.toUserResponse(user), nil
}
func (s *UserService) GetUserByID(id uint) (*model.UserResponse, error) {
user, err := s.userRepo.FindByID(id)
if err != nil {
return nil, errors.New("user not found")
}
return s.toUserResponse(user), nil
}
func (s *UserService) GetAllUsers() ([]model.UserResponse, error) {
users, err := s.userRepo.FindAll()
if err != nil {
return nil, err
}
var responses []model.UserResponse
for _, user := range users {
responses = append(responses, *s.toUserResponse(&user))
}
return responses, nil
}
func (s *UserService) toUserResponse(user *model.User) *model.UserResponse {
return &model.UserResponse{
ID: user.ID,
CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt,
Name: user.Name,
Email: user.Email,
}
}
Binary file not shown.
-83
View File
@@ -1,83 +0,0 @@
package main
import (
"context"
"encoding/json"
"log"
"net/http"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
type Response struct {
Message string `json:"message"`
}
// Example model
type User struct {
gorm.Model
Name string
Email string
}
func getUser(w http.ResponseWriter, r *http.Request) {
db, ok := r.Context().Value("db").(*gorm.DB)
if !ok {
http.Error(w, "Database connection not found", http.StatusInternalServerError)
return
}
var user User
if err := db.First(&user).Error; err != nil {
http.Error(w, "User not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}
func handler(w http.ResponseWriter, r *http.Request) {
// Get DB connection from context
db, ok := r.Context().Value("db").(*gorm.DB)
if !ok {
http.Error(w, "Database connection not found", http.StatusInternalServerError)
return
}
// Example usage
db.AutoMigrate(&User{})
user := User{Name: "Test", Email: "test@example.com"}
db.Create(&user)
w.Header().Set("Content-Type", "application/json")
response := Response{Message: "ok"}
json.NewEncoder(w).Encode(response)
}
func main() {
// Create DB connection string
dsn := "host=db user=postgres password=postgres dbname=mydb port=5432 sslmode=disable TimeZone=UTC"
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatal("Failed to connect to database:", err)
}
// Initialize database
db.AutoMigrate(&User{})
// Setup HTTP server with DB context
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
r = r.WithContext(context.WithValue(r.Context(), "db", db))
handler(w, r)
})
http.HandleFunc("/user", func(w http.ResponseWriter, r *http.Request) {
r = r.WithContext(context.WithValue(r.Context(), "db", db))
getUser(w, r)
})
log.Println("Сервер запущен на http://localhost:8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
@@ -0,0 +1,16 @@
-- Таблица пользователей
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP WITH TIME ZONE NULL
);
-- Индекс для быстрого поиска по email
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
-- Индекс для мягкого удаления
CREATE INDEX IF NOT EXISTS idx_users_deleted_at ON users(deleted_at);
@@ -0,0 +1,45 @@
package database
import (
"fmt"
"log"
"serv_golang_rest_api/internal/config"
"serv_golang_rest_api/internal/model"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
func NewPostgresConnection(cfg *config.Config) (*gorm.DB, error) {
dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=disable TimeZone=UTC",
cfg.DBHost, cfg.DBUser, cfg.DBPassword, cfg.DBName, cfg.DBPort)
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
return nil, fmt.Errorf("failed to connect to database: %w", err)
}
// Автомиграция
if err := autoMigrate(db); err != nil {
return nil, err
}
log.Println("Successfully connected to database")
return db, nil
}
func autoMigrate(db *gorm.DB) error {
models := []interface{}{
&model.User{},
// Добавьте другие модели здесь
}
for _, m := range models {
if err := db.AutoMigrate(m); err != nil {
return fmt.Errorf("failed to migrate model: %w", err)
}
}
log.Println("Database migration completed")
return nil
}