【Go】EchoとSQLiteを使用したAPIサーバー構築チュートリアル

# Go EchoフレームワークとSQLiteを使用したAPIサーバー構築チュートリアル

Go言語はそのシンプルさと高性能から、バックエンド開発において非常に人気があります。
特に、WebフレームワークであるEchoは、その高速な処理能力と使いやすさから、多くの開発者に支持されています。

本記事では、GoのEchoフレームワークとSQLiteデータベースを組み合わせて、基本的なAPIサーバーを構築する方法を詳しく解説します。

## 目次

1. [前提条件](#前提条件)
2. [プロジェクトのセットアップ](#プロジェクトのセットアップ)
3. [必要なパッケージのインストール](#必要なパッケージのインストール)
4. [ディレクトリ構造の作成](#ディレクトリ構造の作成)
5. [`models/user.go` の作成](#modelsusergo-の作成)
6. [`handlers/user.go` の作成](#handlersusergo-の作成)
7. [`routes/routes.go` の作成](#routesroutesgo-の作成)
8. [`main.go` の作成](#maingo-の作成)
9. [サーバーの起動とテスト](#サーバーの起動とテスト)
10. [チェックポイント](#チェックポイント)
11. [まとめ](#まとめ)

---

## 前提条件

- **Goのインストール**: Goがインストールされていることを確認してください。インストールされていない場合は、[公式サイト](https://golang.org/dl/)からインストールしてください。
- **SQLiteの基本的な知識**: SQLiteの基本的な操作方法を理解しているとスムーズです。
- **ターミナルの基本操作**: コマンドラインからの操作に慣れていると便利です。
- **基本的なプログラミング知識**: Go言語の基本的な文法や概念を理解していることが望ましいです。

---

## プロジェクトのセットアップ

まず、プロジェクト用のディレクトリを作成し、Goモジュールを初期化します。これにより、プロジェクト内で依存関係を管理しやすくなります。

```bash
mkdir echo-sqlite-api
cd echo-sqlite-api
go mod init github.com/yourusername/echo-sqlite-api
```

> **注**: `github.com/yourusername/echo-sqlite-api` は適宜変更してください。GitHubなどのリポジトリを使用する場合は、そのURLに合わせてください。

---

## 必要なパッケージのインストール

このプロジェクトでは、以下のパッケージを使用します。

- **Echo**: 高性能なGoのWebフレームワーク。ルーティングやミドルウェアの管理が容易です。
- **GORM**: GoのORM（Object-Relational Mapping）ライブラリ。データベース操作をオブジェクト指向で行うことができます。
- **SQLiteドライバー**: GORM用のSQLiteドライバー。SQLiteデータベースとGORMを連携させるために必要です。

### ORMとGORMについて

ORM（Object-Relational Mapping）は、オブジェクト指向プログラミングとリレーショナルデータベースの間の橋渡しをする技術です。
ORMを使用することで、データベースのテーブルをプログラミング言語のオブジェクトとして扱うことができ、SQLクエリを直接書かずにデータベース操作が可能になります。

**GORM**はGo言語向けの強力なORMライブラリで、シンプルなAPI設計と豊富な機能が特徴です。
GORMを使用することで、データベースのマイグレーション、CRUD操作、リレーションの管理などを効率的に行うことができます。

以下のコマンドで必要なパッケージをインストールします。

```bash
go get -u github.com/labstack/echo/v4
go get -u gorm.io/gorm
go get -u gorm.io/driver/sqlite
```

これらのパッケージをインストールすることで、EchoフレームワークとGORMをプロジェクト内で使用できるようになります。

---

## ディレクトリ構造の作成

プロジェクトのディレクトリ構造を以下のように作成します。適切なディレクトリ構造を持つことで、コードの可読性と保守性が向上します。

```
echo-sqlite-api/
├── handlers/
│   └── user.go
├── models/
│   └── user.go
├── routes/
│   └── routes.go
├── main.go
```

各ディレクトリとファイルを作成します。

```bash
mkdir handlers
mkdir models
mkdir routes
touch main.go
touch models/user.go
touch handlers/user.go
touch routes/routes.go
```

---

## `models/user.go` の作成

`models` フォルダ内に `user.go` ファイルを作成し、`User`モデルを定義します。このモデルはデータベースのテーブル構造を表現します。

```go
// models/user.go
package models

import (
    "gorm.io/gorm"
)

// User モデルの定義
type User struct {
    gorm.Model           // gorm.Model を埋め込むことで、ID、CreatedAt、UpdatedAt、DeletedAt フィールドが自動的に追加されます
    Name  string `json:"name" form:"name"`   // ユーザーの名前を保持します。JSONおよびフォームからのバインディングを可能にします
    Email string `json:"email" form:"email" gorm:"unique"` // ユーザーのメールアドレスを保持します。一意性を持たせるために gorm:"unique" タグを追加
}
```

**説明:**

- **gorm.Model**:
  - `gorm.Model` はGORMが提供する基本的なモデル構造体です。これを埋め込むことで、以下のフィールドが自動的に追加されます。
    - `ID`: プライマリキー
    - `CreatedAt`: レコード作成日時
    - `UpdatedAt`: レコード更新日時
    - `DeletedAt`: レコード削除日時（ソフトデリート用）
  
- **Name フィールド**:
  - ユーザーの名前を保持します。`json` タグと `form` タグにより、JSONリクエストやフォームデータからのバインディングが可能になります。
  
- **Email フィールド**:
  - ユーザーのメールアドレスを保持します。`gorm:"unique"` タグを使用して、一意性制約をデータベースレベルで適用します。これにより、同じメールアドレスのユーザーが複数存在しないようにします。

---

## `handlers/user.go` の作成

`handlers` フォルダ内に `user.go` ファイルを作成し、ユーザー関連のハンドラー関数を定義します。
ハンドラーはAPIエンドポイントへのリクエストを処理する役割を持ちます。

```go
// handlers/user.go
package handlers

import (
    "net/http"
    "strconv"

    "github.com/labstack/echo/v4"
    "github.com/yourusername/echo-sqlite-api/models"
    "gorm.io/gorm"
)

// Handler 構造体にDBインスタンスを持たせる
type Handler struct {
    DB *gorm.DB // GORMのDBインスタンスを保持します。これにより、ハンドラー内でデータベース操作が可能になります
}

// NewHandler は新しいHandlerインスタンスを作成します
func NewHandler(db *gorm.DB) *Handler {
    return &Handler{DB: db}
}

// CreateUser 新しいユーザーを作成します
func (h *Handler) CreateUser(c echo.Context) error {
    user := new(models.User) // 新しいUserインスタンスを作成
    if err := c.Bind(user); err != nil { // リクエストボディをUserインスタンスにバインド
        return c.JSON(http.StatusBadRequest, echo.Map{"error": err.Error()}) // バインドエラー時に400を返す
    }

    if err := h.DB.Create(user).Error; err != nil { // データベースにユーザーを作成
        return c.JSON(http.StatusInternalServerError, echo.Map{"error": err.Error()}) // 作成エラー時に500を返す
    }

    return c.JSON(http.StatusCreated, user) // 成功時に201と作成されたユーザーを返す
}

// GetUsers すべてのユーザーを取得します
func (h *Handler) GetUsers(c echo.Context) error {
    var users []models.User // ユーザーのスライスを用意
    if err := h.DB.Find(&users).Error; err != nil { // データベースから全ユーザーを取得
        return c.JSON(http.StatusInternalServerError, echo.Map{"error": err.Error()}) // 取得エラー時に500を返す
    }

    return c.JSON(http.StatusOK, users) // 成功時に200とユーザー一覧を返す
}

// GetUser 特定のIDを持つユーザーを取得します
func (h *Handler) GetUser(c echo.Context) error {
    id, err := strconv.Atoi(c.Param("id")) // パスパラメータからIDを取得し、整数に変換
    if err != nil {
        return c.JSON(http.StatusBadRequest, echo.Map{"error": "Invalid user ID"}) // IDが無効な場合に400を返す
    }

    var user models.User // Userインスタンスを用意
    if err := h.DB.First(&user, id).Error; err != nil { // データベースからIDに対応するユーザーを取得
        return c.JSON(http.StatusNotFound, echo.Map{"error": "User not found"}) // ユーザーが存在しない場合に404を返す
    }

    return c.JSON(http.StatusOK, user) // 成功時に200とユーザー情報を返す
}

// UpdateUser 特定のユーザー情報を更新します
func (h *Handler) UpdateUser(c echo.Context) error {
    id, err := strconv.Atoi(c.Param("id")) // パスパラメータからIDを取得し、整数に変換
    if err != nil {
        return c.JSON(http.StatusBadRequest, echo.Map{"error": "Invalid user ID"}) // IDが無効な場合に400を返す
    }

    var user models.User // Userインスタンスを用意
    if err := h.DB.First(&user, id).Error; err != nil { // データベースからIDに対応するユーザーを取得
        return c.JSON(http.StatusNotFound, echo.Map{"error": "User not found"}) // ユーザーが存在しない場合に404を返す
    }

    if err := c.Bind(&user); err != nil { // リクエストボディをUserインスタンスにバインド
        return c.JSON(http.StatusBadRequest, echo.Map{"error": err.Error()}) // バインドエラー時に400を返す
    }

    if err := h.DB.Save(&user).Error; err != nil { // データベース上のユーザー情報を更新
        return c.JSON(http.StatusInternalServerError, echo.Map{"error": err.Error()}) // 更新エラー時に500を返す
    }

    return c.JSON(http.StatusOK, user) // 成功時に200と更新されたユーザー情報を返す
}

// DeleteUser 特定のユーザーを削除します
func (h *Handler) DeleteUser(c echo.Context) error {
    id, err := strconv.Atoi(c.Param("id")) // パスパラメータからIDを取得し、整数に変換
    if err != nil {
        return c.JSON(http.StatusBadRequest, echo.Map{"error": "Invalid user ID"}) // IDが無効な場合に400を返す
    }

    if err := h.DB.Delete(&models.User{}, id).Error; err != nil { // データベースからIDに対応するユーザーを削除
        return c.JSON(http.StatusInternalServerError, echo.Map{"error": err.Error()}) // 削除エラー時に500を返す
    }

    return c.NoContent(http.StatusNoContent) // 成功時に204を返し、ボディは空
}
```

**説明:**

- **Handler 構造体**:
  - `Handler` 構造体に `*gorm.DB` インスタンスを保持させることで、ハンドラー関数内でデータベース操作が可能になります。
  - `NewHandler` 関数で `Handler` のインスタンスを作成します。この関数を通じて、DBインスタンスをハンドラーに注入します。

- **ハンドラー関数**:
  - 各関数 (`CreateUser`, `GetUsers`, `GetUser`, `UpdateUser`, `DeleteUser`) は `Handler` のメソッドとして定義され、データベース操作を行います。
  - 各関数内で適切なHTTPステータスコードとレスポンスを返すことで、APIの利用者に対して明確なフィードバックを提供します。

- **エラーハンドリング**:
  - リクエストのバインディングやデータベース操作中に発生したエラーを適切にキャッチし、対応するHTTPステータスコードとエラーメッセージを返します。

- **パッケージのインポート**:
  - `github.com/yourusername/echo-sqlite-api/models` は、実際のプロジェクトパスに置き換えてください。

---

## `routes/routes.go` の作成

`routes` フォルダ内に `routes.go` ファイルを作成し、ルーティングの設定を行います。ルーティングを分離することで、コードの整理がしやすくなります。

```go
// routes/routes.go
package routes

import (
    "github.com/labstack/echo/v4"
    "github.com/yourusername/echo-sqlite-api/handlers"
)

// SetupRoutes ルートを設定します
func SetupRoutes(e *echo.Echo, h *handlers.Handler) {
    // ルート (`/`) を設定し、簡単なメッセージを返します
    e.GET("/", func(c echo.Context) error {
        return c.String(200, "Hello, Echo with SQLite!") // ルートにアクセスすると固定メッセージを返します
    })

    // APIルートの設定
    api := e.Group("/api") // `/api` グループを作成
    users := api.Group("/users") // `/api/users` グループを作成
    users.POST("", h.CreateUser) // POST `/api/users` にCreateUserハンドラーを割り当て
    users.GET("", h.GetUsers) // GET `/api/users` にGetUsersハンドラーを割り当て
    users.GET("/:id", h.GetUser) // GET `/api/users/:id` にGetUserハンドラーを割り当て
    users.PUT("/:id", h.UpdateUser) // PUT `/api/users/:id` にUpdateUserハンドラーを割り当て
    users.DELETE("/:id", h.DeleteUser) // DELETE `/api/users/:id` にDeleteUserハンドラーを割り当て
}
```

**説明:**

- **SetupRoutes 関数**:
  - Echoのインスタンス (`e *echo.Echo`) とハンドラー (`h *handlers.Handler`) を受け取り、ルーティングを設定します。
  - ルート (`/`) とAPIエンドポイント (`/api/users`) を設定し、それぞれに対応するハンドラー関数を割り当てます。
  - グループ化 (`Group`) を使用することで、APIのエンドポイントを整理しやすくなります。

- **パッケージのインポート**:
  - `github.com/yourusername/echo-sqlite-api/handlers` は、実際のプロジェクトパスに置き換えてください。

---

## `main.go` の作成

プロジェクトのルートディレクトリに `main.go` ファイルを作成し、アプリケーションのエントリーポイントを定義します。
このファイルでは、データベースの初期化、Echoインスタンスの作成、ルーティングの設定、サーバーの起動を行います。

```go
// main.go
package main

import (
    "log"

    "github.com/labstack/echo/v4"
    "github.com/yourusername/echo-sqlite-api/handlers"
    "github.com/yourusername/echo-sqlite-api/models"
    "github.com/yourusername/echo-sqlite-api/routes"
    "gorm.io/driver/sqlite"
    "gorm.io/gorm"
)

func main() {
    // データベースの初期化
    db, err := gorm.Open(sqlite.Open("users.db"), &gorm.Config{}) // SQLiteデータベースに接続
    if err != nil {
        log.Fatal("Failed to connect to database:", err) // 接続失敗時にログを出力して終了
    }

    // マイグレーション
    err = db.AutoMigrate(&models.User{}) // Userモデルに基づいてテーブルを自動生成・更新
    if err != nil {
        log.Fatal("Failed to migrate database:", err) // マイグレーション失敗時にログを出力して終了
    }

    // Echoインスタンスの作成
    e := echo.New() // 新しいEchoインスタンスを作成

    // ハンドラーの初期化
    h := handlers.NewHandler(db) // DBインスタンスをハンドラーに注入

    // ルーティングの設定
    routes.SetupRoutes(e, h) // ルーティング設定を適用

    // サーバーの起動
    log.Println("Server is running on http://localhost:8080") // サーバー起動メッセージをログに出力
    if err := e.Start(":8080"); err != nil { // ポート8080でサーバーを起動
        log.Fatal("Failed to start server:", err) // 起動失敗時にログを出力して終了
    }
}
```

**説明:**

1. **データベースの初期化**:
    - `gorm.Open` を使用してSQLiteデータベース (`users.db`) に接続します。
    - データベース接続に失敗した場合、ログを出力してアプリケーションを終了します。

2. **マイグレーション**:
    - `db.AutoMigrate(&models.User{})` を使用して、`User`モデルに基づいてデータベーステーブルを自動生成・更新します。
    - これにより、モデルの変更がデータベースに反映されます。

3. **Echoインスタンスの作成**:
    - `echo.New()` でEchoのインスタンスを作成します。このインスタンスを通じて、ルーティングやミドルウェアの設定を行います。

4. **ハンドラーの初期化**:
    - `handlers.NewHandler(db)` で `Handler` のインスタンスを作成し、データベースインスタンスを注入します。これにより、ハンドラー内でデータベース操作が可能になります。

5. **ルーティングの設定**:
    - `routes.SetupRoutes(e, h)` を呼び出し、ルーティングを別ファイルで設定します。これにより、メインファイルがシンプルに保たれます。

6. **サーバーの起動**:
    - サーバーをポート `8080` で起動します。サーバーが正常に起動すると、ログに起動メッセージが表示されます。
    - サーバーの起動に失敗した場合、ログを出力してアプリケーションを終了します。

---

## サーバーの起動とテスト

すべての設定が完了したら、サーバーを起動してAPIをテストします。以下の手順に従って進めてください。

### サーバーの起動

以下のコマンドを実行して、サーバーを起動します。

```bash
go run main.go
```

サーバーが起動すると、以下のようなメッセージが表示されます。

```
Server is running on http://localhost:8080
```

### APIのテスト

以下のツールを使用してAPIをテストできます。

- **cURL**: コマンドラインからリクエストを送信。
- **Postman**: GUIベースのAPIテストツール。
- **Insomnia**: もう一つのGUIベースのAPIクライアント。

ここでは、cURLを使用した例を紹介します。

#### 1. ユーザーの作成

新しいユーザーを作成するには、以下のcURLコマンドを実行します。

```bash
curl -X POST http://localhost:8080/api/users \
-H "Content-Type: application/json" \
-d '{"name":"John Doe","email":"john@example.com"}'
```

**期待されるレスポンス**:

```json
{
    "ID": 1,
    "CreatedAt": "2024-04-27T12:00:00Z",
    "UpdatedAt": "2024-04-27T12:00:00Z",
    "DeletedAt": null,
    "Name": "John Doe",
    "Email": "john@example.com"
}
```

**説明**:
- このリクエストは、新しいユーザー「John Doe」をデータベースに作成します。`Content-Type` ヘッダーを `application/json` に設定し、ユーザーの名前とメールアドレスをJSON形式で送信します。

#### 2. 全ユーザーの取得

全てのユーザーを取得するには、以下のcURLコマンドを実行します。

```bash
curl http://localhost:8080/api/users
```

**期待されるレスポンス**:

```json
[
    {
        "ID": 1,
        "CreatedAt": "2024-04-27T12:00:00Z",
        "UpdatedAt": "2024-04-27T12:00:00Z",
        "DeletedAt": null,
        "Name": "John Doe",
        "Email": "john@example.com"
    }
]
```

**説明**:
- このリクエストは、データベースに存在する全てのユーザーを取得します。レスポンスはユーザーの配列となります。

#### 3. 特定のユーザーの取得

特定のIDを持つユーザーを取得するには、以下のcURLコマンドを実行します。

```bash
curl http://localhost:8080/api/users/1
```

**期待されるレスポンス**:

```json
{
    "ID": 1,
    "CreatedAt": "2024-04-27T12:00:00Z",
    "UpdatedAt": "2024-04-27T12:00:00Z",
    "DeletedAt": null,
    "Name": "John Doe",
    "Email": "john@example.com"
}
```

**説明**:
- このリクエストは、IDが1のユーザーを取得します。指定されたIDに対応するユーザーが存在しない場合、404エラーが返されます。

#### 4. ユーザーの更新

既存のユーザー情報を更新するには、以下のcURLコマンドを実行します。

```bash
curl -X PUT http://localhost:8080/api/users/1 \
-H "Content-Type: application/json" \
-d '{"name":"Jane Doe","email":"jane@example.com"}'
```

**期待されるレスポンス**:

```json
{
    "ID": 1,
    "CreatedAt": "2024-04-27T12:00:00Z",
    "UpdatedAt": "2024-04-27T12:30:00Z",
    "DeletedAt": null,
    "Name": "Jane Doe",
    "Email": "jane@example.com"
}
```

**説明**:
- このリクエストは、IDが1のユーザーの名前とメールアドレスを更新します。`Content-Type` ヘッダーを `application/json` に設定し、更新内容をJSON形式で送信します。

#### 5. ユーザーの削除

特定のユーザーを削除するには、以下のcURLコマンドを実行します。

```bash
curl -X DELETE http://localhost:8080/api/users/1
```

**期待されるレスポンス**:
ステータスコード `204 No Content` が返され、ボディは空です。

**説明**:
- このリクエストは、IDが1のユーザーをデータベースから削除します。成功時にはステータスコード204が返され、レスポンスボディは空となります。

---

## チェックポイント

APIサーバーを正しく構築・動作させるために、以下のポイントを確認してください。

1. **パッケージパスの確認**:
    - `main.go` 内のインポートパス `github.com/yourusername/echo-sqlite-api/handlers`, `github.com/yourusername/echo-sqlite-api/models`, および `github.com/yourusername/echo-sqlite-api/routes` が正しいことを確認してください。
    - 実際のプロジェクトパスに合わせて変更してください。特に、GitHubなどのリポジトリを使用する場合は、正確なURLに設定する必要があります。

2. **エクスポートの確認**:
    - `models.User` 構造体が大文字で始まっているため、他のパッケージからアクセス可能です。
    - ハンドラー関数 (`CreateUser` など) も大文字で始まっているため、エクスポートされています。Goでは、名前が大文字で始まるとパブリック（エクスポートされた）ものとして扱われます。

3. **依存関係の確認**:
    - `go.mod` ファイルに必要な依存関係が正しく記載されていることを確認してください。以下は `go.mod` の一例です。

    ```go
    module github.com/yourusername/echo-sqlite-api

    go 1.20

    require (
        github.com/labstack/echo/v4 v4.9.0
        gorm.io/driver/sqlite v1.4.6
        gorm.io/gorm v1.25.1
    )
    ```

    - 不足があれば、再度 `go get` コマンドを実行して依存関係を追加してください。

4. **ルーティングの設定確認**:
    - `routes/routes.go` 内で正しくルートが設定されていることを確認してください。
    - ハンドラーが正しく `handlers.NewHandler(db)` から渡されていることを確認してください。これにより、ハンドラー内でデータベース操作が可能になります。

5. **データベースファイルの確認**:
    - プロジェクトのルートディレクトリに `users.db` ファイルが作成されていることを確認してください。GORMが自動的にこのファイルを作成しますが、権限の問題などで失敗する場合があります。

6. **ポートの競合確認**:
    - 使用するポート（デフォルトは8080）が他のプロセスと競合していないことを確認してください。競合がある場合は、`main.go` の `e.Start(":8080")` を別のポートに変更してください。

---

## まとめ

今回のチュートリアルでは、**`models` フォルダ**と **`handlers` フォルダ** を使用しながら、**ルーティングを別ファイル (`routes/routes.go`) に分離**する方法で、GoのEchoフレームワークを使用したSQLiteと連携するAPIサーバーを構築しました。この構成により、コードの可読性と保守性を向上させつつ、パッケージスコープによるエラーを回避することができます。

**参考資料:**

- [Echo Web Framework](https://echo.labstack.com/)
- [GORM Documentation](https://gorm.io/docs/)
- [SQLite Official Site](https://www.sqlite.org/index.html)

---
### 会社紹介

**株式会社 Mosaica**  
最先端テクノロジーで社会課題を解決し、持続可能な未来を創造する IT カンパニー。
AI ソリューション、クラウド統合、DX 推進、経営コンサルティングなど包括的なサービスでビジネス変革を支援しています。  

詳しくは [公式サイト](https://mosaica.co.jp/) までお気軽にご相談ください。
公式サイト： https://mosaica.co.jp/