🌐

【完全解説】GolangとGORMで作るAPIベースのメモアプリ

に公開

1. はじめに

これまで、GolangとGORMを用いてSQLiteと連携したメモアプリの作成方法を学んできました。しかし、実際のアプリケーションでは、単にコマンドラインで動作するだけでなく、外部からAPIとしてアクセスできることが求められるケースが多くあります。本記事では、既存のGORMとSQLiteによるCRUD操作を活かしながら、HTTP APIサーバーとしてメモアプリを構築する方法に焦点を当てて解説します。

この記事では、以下の点に注目します。

  • Golangの標準ライブラリを使ったHTTPサーバーの基本構築方法
  • APIエンドポイントの設計とルーティングの実装
  • JSONのパースやレスポンス生成、適切なHTTPステータスコードの返し方

これにより、APIとして外部からメモの追加、一覧表示、削除ができるようになり、Webアプリケーションや他のサービスとの連携が容易になります。

2. APIサーバー構築の基礎

Golangでは、標準ライブラリの net/http パッケージを使ってシンプルなHTTPサーバーを立ち上げることができます。ここでは、基本的なサーバーの起動方法と、リクエストのルーティング方法について解説します。

HTTPサーバーの起動

http.ListenAndServe を使うことで、指定したポートでサーバーを起動できます。例えば、ポート8080でサーバーを起動する場合は以下のように記述します。

package main

import (
	"fmt"
	"log"
	"net/http"
)

func main() {
	fmt.Println("Server is running on :8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

このコードを実行すると、ポート8080でHTTPサーバーが起動し、リクエストを待ち受ける状態になります。

ルーティングの基本

net/http パッケージの http.HandleFunc 関数を使うことで、特定のURLパスに対するハンドラー関数を登録できます。たとえば、以下のようにエンドポイントを定義します。

package main

import (
	"fmt"
	"log"
	"net/http"
)

func main() {
	http.HandleFunc("/memo", memoHandler)
	http.HandleFunc("/memo/", memoHandlerWithID) // DELETE 用など、ID付きのルート

	fmt.Println("Server is running on :8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

func memoHandler(w http.ResponseWriter, r *http.Request) {
	// ここで GET や POST の処理を振り分ける
}

func memoHandlerWithID(w http.ResponseWriter, r *http.Request) {
	// URLパスからIDを取り出し、DELETEの処理を実装
}

このように、URLパスごとに異なるハンドラー関数を登録することで、エンドポイントごとの処理を柔軟に実装できます。
たとえば、 /memo にはPOSTリクエストでメモ追加、GETリクエストで一覧表示を、 /memo/{id} にはDELETEリクエストでメモ削除を行う、といった振り分けが可能です。

リクエストとレスポンスの基本

APIサーバーでは、リクエストボディからJSONをパースしたり、レスポンスとしてJSONを返すことが一般的です。
標準ライブラリの encoding/json を利用すると、以下のように簡単にJSON操作が行えます。

  • JSON のパース:
var memo Memo
err := json.NewDecoder(r.Body).Decode(&memo)
if err != nil {
    http.Error(w, "Bad request", http.StatusBadRequest)
    return
}
  • JSON のレスポンス生成:
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(memo)

このような基本的な構造を抑えることで、後のセクションで具体的なCRUD操作の実装にスムーズに移行できます。

3. エンドポイント設計

ここでは、今回作成するメモアプリのAPIエンドポイントをどのように設計するかについて解説します。APIエンドポイントは、クライアントとサーバー間の通信窓口となる部分で、各リクエストに対してどの処理を実行するかを決めます。今回のメモアプリでは、以下の3種類の操作を行います。

  • メモの追加: 新しいメモを登録する
  • メモの一覧表示: すべてのメモを取得して表示する
  • メモの削除: 指定したIDのメモを削除する

これらの操作に対して、以下のようなエンドポイントを設計します。

POST /memo

  • 目的:
    ユーザーが新しいメモの内容を送信すると、その内容をデータベースに保存します。
  • リクエスト:
    リクエストボディには、JSON形式でメモの内容を含みます。
    例:
    {
        "content": "今日のタスクを確認する"
    }
    
  • レスポンス:
    保存されたメモのレコード(自動生成されたIDと内容)をJSON形式で返します。

GET /memo

  • 目的:
    データベースから全てのメモレコードを取得し、クライアントに一覧として返します。
  • リクエスト:
    クエリパラメータは特に必要なく、単にGETリクエストを送信します。
  • レスポンス:
    JSON配列形式で、各メモのIDと内容を返します。
    例:
    [
      {"id": 1, "content": "今日のタスクを確認する"},
      {"id": 2, "content": "ミーティングの議事録を作成する"}
    ]
    

DELETE /memo/{id}

  • 目的:
    URLパスに指定されたIDのメモをデータベースから削除します。
  • リクエスト:
    エンドポイントのパスにメモのIDを含めてDELETEリクエストを送信します。
    例: /memo/3
  • レスポンス:
    削除に成功した場合は成功メッセージを、存在しないIDの場合はエラーメッセージを返します。

エンドポイント設計のポイント

  • シンプルで直感的なURL:
    /memo および /memo/{id} のように、リソース名(メモ)を直接URLに表現することで、どのリソースに対する操作かが一目で分かるようにします。

  • HTTPメソッドの適切な利用:

    • POST はリソースの作成に使い、
    • GET はリソースの取得に使い、
    • DELETE はリソースの削除に使うことで、RESTの原則に沿ったAPI設計となります。
  • JSONによるデータのやり取り:
    リクエストとレスポンスをJSON形式で行うことで、フロントエンドとの連携や、他のサービスとの統合が容易になります。

この設計により、シンプルかつ直感的なAPIが実現され、後の実装ステップで具体的なCRUD処理をGORMを使って行う際の土台となります。次のセクションでは、これらのエンドポイントを用いた具体的な実装方法について詳しく見ていきます。

4. JSONパースとレスポンスの生成

APIサーバーでは、クライアントとデータをやり取りするために、JSON形式のリクエストとレスポンスが非常に重要です。このセクションでは、Golangの標準ライブラリ encoding/json を用いて、JSONのパース(リクエストボディからのデコード)とレスポンス生成(エンコード)を行う方法を解説します。

JSONパース

クライアントから送られてくるリクエストボディには、メモの内容などのデータがJSON形式で含まれています。
このJSONデータをGolangの構造体に変換するには、json.NewDecoder(r.Body).Decode(&target) を利用します。
たとえば、メモ追加用のリクエストの場合、以下のようにしてJSONをパースできます。

func addMemo(w http.ResponseWriter, r *http.Request) {
	var memo Memo
	// リクエストボディからJSONをでコードして memo に格納する
	if err := json.NewDecoder(r.Body).Decode(&memo); err != nil {
		http.Error(w, "Bad request", http.StatusBadRequest)
		return
	}
	// DB への保存処理は後程実装 (GORMを利用)
	result := db.Create(&memo)
	if result.Error != nil {
		http.Error(w, result.Error.Error(), http.StatusInternalServerError)
		return
	}
	// JSON 形式で追加したメモをレスポンスとして返す
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(memo)
}

ポイント

  • エラーチェック:
    JSONのデコードに失敗した場合、http.Error を使って適切なHTTPステータスコード(400 Bad Requestなど)でエラーレスポンスを返します。

  • 構造体とのマッピング:
    事前に定義した Memo 構造体にJSONデータをマッピングすることで、リクエスト内容を簡単に利用できるようになります。

レスポンス生成

APIサーバーでは、DBから取得したデータや処理結果をJSON形式でクライアントに返すことが一般的です。
json.NewEncoder(w).Encode(data) を使うと、指定したデータ(例えば構造体やスライス)を自動的にJSONに変換してレスポンスボディに書き込みます。

たとえば、メモ一覧を取得して返す場合は次のようになります。

func listMemos(w http.ResponseWriter, _ *http.Request) {
	var memos []Memo
	result := db.Find(&memos)
	if result.Error != nil {
		http.Error(w, result.Error.Error(), http.StatusInternalServerError)
		return
	}
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(memos)
}

ポイント

  • Content-Typeの設定:
    レスポンスヘッダーに "Content-Type": "application/json" をセットすることで、クライアント側に返されるデータがJSON形式であることを明示します。

  • 自動エンコード:
    json.NewEncoder(w).Encode() を使用することで、手動でJSON文字列を生成する手間を省き、エンコードエラーも自動的に処理できます。

5. エラーハンドリングとステータスコード管理

APIサーバーでは、正しいエラーハンドリングとHTTPステータスコードの管理が非常に重要です。適切なエラーレスポンスを返すことで、クライアント側に何が問題かを明確に伝えるとともに、成功時や失敗時の状況を正確に示すことができます。

HTTPステータスコードの基本

  • 200 OK: リクエストが成功した場合に返す。
  • 201 Created: 新しいリソースの作成が成功した場合に返す(例: メモ追加)。
  • 400 Bad Request: リクエスト内容が不正な場合に返す(例: JSONのパース失敗)。
  • 404 Not Found: 指定されたリソースが存在しない場合に返す(例: 削除対象のメモが見つからない)。
  • 405 Method Not Allowed: サポートされていないHTTPメソッドが使われた場合に返す。
  • 500 Internal Server Error: サーバー側で予期しないエラーが発生した場合に返す。

エラーハンドリングの実装例

Golangの net/http パッケージでは、http.Error 関数を使って簡単にエラーレスポンスを返すことができます。以下は、メモの追加処理におけるエラーハンドリングの例です。

func addMemo(w http.ResponseWriter, r *http.Request) {
	var memo Memo
	// リクエストボディからJSONをデコードして memo に格納する
	if err := json.NewDecoder(r.Body).Decode(&memo); err != nil {
		// JSON のパースに失敗した場合、 400 Bad Request を返す
		http.Error(w, "Bad request", http.StatusBadRequest)
		return
	}
	result := db.Create(&memo)
	if result.Error != nil {
		// DB操作中にエラーが発生した場合、500 Internal Server Error を返す
		http.Error(w, result.Error.Error(), http.StatusInternalServerError)
		return
	}
	// 成功時は、 201 Created のステータスコードと共に、作成されたメモの情報を返す
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusCreated)
	json.NewEncoder(w).Encode(memo)
}

また、削除処理では、指定IDのリソースが見つからない場合に404 Not Foundを返す例も示します。

func deleteMemo(w http.ResponseWriter, _ *http.Request, id uint){
	result := db.Delete(&Memo{}, id)
	if result.Error != nil {
		http.Error(w, result.Error.Error(), http.StatusInternalServerError)
		return
	}
	if result.RowsAffected == 0 {
		// 指定したIDのメモが存在しない場合、 404 Not Found を返す
		http.Error(w, "No memo found with given ID", http.StatusNotFound)
		return
	}
	// 削除成功時は 200 OK で成功メッセージを返す
	w.WriteHeader(http.StatusOK)
	fmt.Fprintln(w, "Memo deleted successfully")
}

まとめ

  • エラーチェック: リクエスト処理中に発生するエラー(JSONのパース、DB操作など)を検出し、適切なHTTPステータスコード(400, 404, 500など)でクライアントに通知します。
  • 成功時のレスポンス: 成功した場合は200番台のステータスコード(200 OK、201 Createdなど)を返し、結果をJSON形式で返します。
  • 標準ライブラリの活用: http.Error と w.WriteHeader を用いることで、エラーレスポンスや成功レスポンスを簡潔に生成できます。

これにより、APIサーバーとして信頼性の高いレスポンスが実現でき、クライアントは問題点や成功の状態を正確に把握できるようになります。

6. 実装例と動作確認

ここまでの各セクションで、HTTPサーバーの起動、ルーティング、エンドポイント設計、JSONパース・レスポンス生成、そしてエラーハンドリングの基本を学びました。
このセクションでは、これらを統合した最終的なAPIサーバーのコード例と、その動作確認方法を示します。

最終コード例

以下は、GORMとSQLiteを使って構築した、メモの追加、一覧表示、削除ができるAPIサーバーの全体コードです。
このコードをビルドすると、ポート8080でAPIサーバーが起動し、各エンドポイントに対してHTTPリクエストで操作が可能になります。

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"strconv"

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

type Memo struct {
	ID      uint   `gorm:"primaryKey" json:"id"`
	Content string `gorm:"not null" json:"content"`
}

var db *gorm.DB

func main() {
	var err error
	// SQLite データベースに接続。ファイルが存在しない場合は自動生成される
	db, err = gorm.Open(sqlite.Open("memo.db"), &gorm.Config{})
	if err != nil {
		log.Fatal("failed to connect database:", err)
	}

	// 自動マイグレーションにより、 Memo モデルに基づいたテーブルを作成
	db.AutoMigrate(&Memo{})

	// エンドポイントの定義
	// /memo では GET と POST を処理
	http.HandleFunc("/memo", memoHandler)
	// /memo/ 以降は DELETE 用など、URLパスにIDを含むリクエストを処理
	http.HandleFunc("/memo/", memoHandlerWithID)

	fmt.Println("Server is running on :8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

// memoHandler は、 GETリクエストで一覧表示、POSTリクエストでメモの追加を行う
func memoHandler(w http.ResponseWriter, r *http.Request) {
	switch r.Method {
	case "GET":
		listMemos(w, r)
	case "POST":
		addMemo(w, r)
	default:
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
	}
}

// memoHandlerWithID は、URLパスからIDを取得し、DELETEリクエストでメモを削除する
func memoHandlerWithID(w http.ResponseWriter, r *http.Request) {
	if r.Method != "DELETE" {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}
	// URL例: /memo/3 から "3" を抽出
	idStr := r.URL.Path[len("/memo/"):]
	id, err := strconv.Atoi(idStr)
	if err != nil {
		http.Error(w, "Invalid ID", http.StatusBadRequest)
		return
	}
	deleteMemo(w, r, uint(id))
}

func addMemo(w http.ResponseWriter, r *http.Request) {
	var memo Memo
	// リクエストボディからJSONをデコードして memo に格納する
	if err := json.NewDecoder(r.Body).Decode(&memo); err != nil {
		// JSON のパースに失敗した場合、 400 Bad Request を返す
		http.Error(w, "Bad request", http.StatusBadRequest)
		return
	}
	result := db.Create(&memo)
	if result.Error != nil {
		// DB操作中にエラーが発生した場合、500 Internal Server Error を返す
		http.Error(w, result.Error.Error(), http.StatusInternalServerError)
		return
	}
	// 成功時は、 201 Created のステータスコードと共に、作成されたメモの情報を返す
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusCreated)
	json.NewEncoder(w).Encode(memo)
}

func listMemos(w http.ResponseWriter, _ *http.Request) {
	var memos []Memo
	result := db.Find(&memos)
	if result.Error != nil {
		http.Error(w, result.Error.Error(), http.StatusInternalServerError)
		return
	}
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(memos)
}

func deleteMemo(w http.ResponseWriter, _ *http.Request, id uint) {
	result := db.Delete(&Memo{}, id)
	if result.Error != nil {
		http.Error(w, result.Error.Error(), http.StatusInternalServerError)
		return
	}
	if result.RowsAffected == 0 {
		// 指定したIDのメモが存在しない場合、 404 Not Found を返す
		http.Error(w, "No memo found with given ID", http.StatusNotFound)
		return
	}
	// 削除成功時は 200 OK で成功メッセージを返す
	w.WriteHeader(http.StatusOK)
	fmt.Fprintln(w, "Memo deleted successfully")
}

動作確認

コードを保存し、以下の手順でAPIサーバーが正常に動作するか確認してください。

1. サーバーの起動

go run main.go

起動すると、以下のメッセージが表示されます:

Server is running on :8080

2. メモの追加 (POST /memo)

curl コマンドを使って、新しいメモを追加します。
例として、「今日のタスクを確認する」というメモを追加する場合:

curl -X POST -H "Content-Type: application/json" -d '{"content": "今日のタスクを確認する"}' http://localhost:8080/memo

成功すると、以下のようなJSONレスポンスが返され、作成されたメモのIDが確認できます。

{"id":1,"content":"今日のタスクを確認する"}

3. メモの一覧表示 (GET /memo)

以下のコマンドを実行して、保存されたメモ一覧を取得します:

curl http://localhost:8080/memo

レスポンスは以下のようなJSON配列になります:

[{"id":1,"content":"今日のタスクを確認する"}]

4. メモの削除 (DELETE /memo/{id})

例えば、IDが1のメモを削除する場合は、以下のコマンドを実行します:

curl -X DELETE http://localhost:8080/memo/1

成功すると、レスポンスに「Memo deleted successfully」と表示されます。

7. まとめ

7. まとめ

今回の記事では、GolangとGORMを活用してSQLiteと連携するAPIベースのメモアプリを構築する方法を、ステップバイステップで解説しました。
ORMを利用することで、直接SQL文を書く場合に比べ、コードが大幅に簡潔になり、保守性や拡張性が向上する点を実感できたと思います。また、HTTP APIサーバーとして実装することで、外部からのアクセスや他サービスとの連携が容易になることも理解できたでしょう。

この記事で学んだ主なポイントは以下の通りです。

  • Golangの標準ライブラリを用いたHTTPサーバーの起動とルーティングの基本
  • シンプルで直感的なエンドポイント設計(POST /memo, GET /memo, DELETE /memo/{id})
  • JSONパースおよびレスポンス生成の実装方法
  • 適切なエラーハンドリングとHTTPステータスコード管理の重要性
  • GORMを使ったモデル定義、自動マイグレーション、CRUD操作の簡潔な実装

この基盤を活かし、さらに高度な機能やセキュリティ対策、パフォーマンスの最適化に取り組むことで、実際のWebアプリケーション開発にも応用できる知識が身につくでしょう。
ぜひ、今回学んだ技術を応用して、さらに多機能なAPIサーバーの開発に挑戦してください!

Discussion