Gin基礎 #7 プロジェクト構成とミニREST API
前回の記事までで、ルーティング、検証、レスポンス、ミドルウェア、データベースをすべて扱いました。最後の記事では、これまで 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パスのような値は、コードに直接埋め込まず環境変数に出す方がよいです。開発と運用で異なる値を使いやすくなります。
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 の接続コードを別のパッケージに移します。
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 #
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 のクロージャの代わりに、構造体に依存性を持たせる方式に変えます。ハンドラが増えるほど、こちらがすっきりします。
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)
}UserHandler が DB フィールドを持ち、各メソッドがそれを共有します。ハンドラをメソッドにまとめると、ルート登録もすっきりします。
ルートの分離 — routes #
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 は、各ピースを読み込んでつなぐ役割だけを担います。コードが短く、全体の流れが一目で見えます。
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デプロイまで扱います。実戦のサービスに近い形へと発展させていきます。