Gin基礎 #7 プロジェクト構成とミニREST API

読了 4分

前回の記事までで、ルーティング、検証、レスポンス、ミドルウェア、データベースをすべて扱いました。最後の記事では、これまで main.go の1つのファイルにまとめてきたコードをレイヤーごとに分け、設定を分離して、小さなREST APIの骨組みを完成させます。Gin基礎シリーズの締めくくりです。

  • #1 はじめてのサーバー
  • #2 ルーティングとハンドラ
  • #3 リクエストのバインディングと検証
  • #4 レスポンス処理 — JSON、ステータスコード、エラー
  • #5 ミドルウェア
  • #6 データベース連携 (GORM)
  • #7 プロジェクト構成とミニREST API ← 今回の記事

1つのファイルの限界 #

これまではすべてのコードを main.go にまとめてきました。学習の段階では一目で見えて便利ですが、ハンドラが増えてモデルが多くなると、すぐに限界が来ます。ルーティングとビジネスロジックとDBアクセスが1か所に混ざり、どこを直せばよいか見つけにくくなります。

解決策は役割ごとにファイルを分けることです。小さなプロジェクトでも、次の程度の区分があれば十分です。

フォルダ構成
gin-app/
├── main.go            # エントリーポイント、組み立て
├── config/
│   └── config.go      # 設定
├── database/
│   └── database.go    # DB接続
├── models/
│   └── user.go        # データモデル
├── handlers/
│   └── user.go        # リクエスト処理
└── routes/
    └── routes.go      # ルート登録

大げさなアーキテクチャではなく、「設定」「接続」「モデル」「ハンドラ」「ルート」へと関心事を分けた程度です。規模が大きくなればハンドラとDBアクセスの間にサービス層をさらに置くこともありますが、入門の段階ではこの構成から始めることをおすすめします。

設定の分離 — config #

ポート、DBパスのような値は、コードに直接埋め込まず環境変数に出す方がよいです。開発と運用で異なる値を使いやすくなります。

config/config.go
package config

import "os"

type Config struct {
	Port   string
	DBPath string
}

func Load() Config {
	return Config{
		Port:   getEnv("PORT", "8080"),
		DBPath: getEnv("DB_PATH", "app.db"),
	}
}

func getEnv(key, fallback string) string {
	if v := os.Getenv(key); v != "" {
		return v
	}
	return fallback
}

環境変数がなければデフォルト値を使うシンプルなパターンです。.env ファイルを使いたければ github.com/joho/godotenv のようなライブラリを加えられますが、必須ではありません。

DB接続の分離 — database #

#6 の接続コードを別のパッケージに移します。

database/database.go
package database

import (
	"log"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"

	"gin-app/models"
)

func Connect(dbPath string) *gorm.DB {
	db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
	if err != nil {
		log.Fatal("DB接続失敗:", err)
	}
	db.AutoMigrate(&models.User{})
	return db
}

モデルの分離 — models #

models/user.go
package models

import "time"

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

ハンドラの分離 — handlers #

ハンドラが *gorm.DB を必要とするので、#6 のクロージャの代わりに、構造体に依存性を持たせる方式に変えます。ハンドラが増えるほど、こちらがすっきりします。

handlers/user.go
package handlers

import (
	"errors"
	"net/http"

	"github.com/gin-gonic/gin"
	"gorm.io/gorm"

	"gin-app/models"
)

type UserHandler struct {
	DB *gorm.DB
}

func (h *UserHandler) Create(c *gin.Context) {
	var req struct {
		Name  string `json:"name" binding:"required"`
		Email string `json:"email" binding:"required,email"`
	}
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	user := models.User{Name: req.Name, Email: req.Email}
	if err := h.DB.Create(&user).Error; err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "保存失敗"})
		return
	}
	c.JSON(http.StatusCreated, user)
}

func (h *UserHandler) Get(c *gin.Context) {
	var user models.User
	if err := h.DB.First(&user, c.Param("id")).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			c.JSON(http.StatusNotFound, gin.H{"error": "ユーザーが見つかりません"})
			return
		}
		c.JSON(http.StatusInternalServerError, gin.H{"error": "照会失敗"})
		return
	}
	c.JSON(http.StatusOK, user)
}

func (h *UserHandler) List(c *gin.Context) {
	var users []models.User
	if err := h.DB.Find(&users).Error; err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "照会失敗"})
		return
	}
	c.JSON(http.StatusOK, users)
}

UserHandlerDB フィールドを持ち、各メソッドがそれを共有します。ハンドラをメソッドにまとめると、ルート登録もすっきりします。

ルートの分離 — routes #

routes/routes.go
package routes

import (
	"github.com/gin-gonic/gin"
	"gorm.io/gorm"

	"gin-app/handlers"
)

func Register(r *gin.Engine, db *gorm.DB) {
	userHandler := &handlers.UserHandler{DB: db}

	users := r.Group("/users")
	{
		users.POST("", userHandler.Create)
		users.GET("", userHandler.List)
		users.GET("/:id", userHandler.Get)
	}
}

組み立て — main.go #

最後に main.go は、各ピースを読み込んでつなぐ役割だけを担います。コードが短く、全体の流れが一目で見えます。

main.go
package main

import (
	"github.com/gin-gonic/gin"

	"gin-app/config"
	"gin-app/database"
	"gin-app/routes"
)

func main() {
	cfg := config.Load()
	db := database.Connect(cfg.DBPath)

	r := gin.Default()
	routes.Register(r, db)

	r.Run(":" + cfg.Port)
}

設定を読み、DBに接続し、ルーターを作ってルートを登録したあとサーバーを起動します。各ステップが別々のパッケージにあるので、ハンドラを追加したりDBを替えたりしても、手を入れる場所が明確です。

シリーズを終えて #

Gin基礎シリーズで扱った内容を整理します。

  • 最初のサーバーと net/http に対するGinの利点 (#1)
  • ルーティング、パス、クエリパラメータ、ルーターグループ (#2)
  • リクエストのバインディングと検証 (#3)
  • レスポンス形式、ステータスコード、一貫したエラー (#4)
  • ミドルウェアとコンテキストでの値の受け渡し (#5)
  • GORMでのCRUD (#6)
  • レイヤーごとのプロジェクト構成 (今回の記事)

ここまでで、小さなREST APIを最初から最後まで作れます。次のステップであるGin中級シリーズでは、JWT認証、ミドルウェアベースの集中エラー処理、依存性注入、テスト、ページネーション、そしてDockerデプロイまで扱います。実戦のサービスに近い形へと発展させていきます。

X