GoでREST APIを作ろう
この章では、Gin と GORM を使ってノートアプリケーションのREST APIを構築するハンズオン形式で学習します。これにより、Gin と GORM を組み合わせた実用的なREST APIを自分で作れるようになります。
1. 本章の概要
1.1 本章の目的
GoでREST APIを実装する場合、WebフレームワークのGinとORMのGORMを組み合わせるのが一般的な選択肢のひとつです。前章までで学んだGin(ルーティング・リクエスト処理)と GORM(モデル定義とCRUD)を組み合わせて、ノートアプリケーションの5つのCRUDエンドポイントを持つREST APIを実装します。
1.2 ハンズオンの流れ
ノートを扱う5つのCRUDエンドポイントを、Gin と GORM を使って実装します。データベース(notes_api)を先に用意し、curl で各エンドポイントの動作を確認します。実装するエンドポイントの一覧と役割は、次の「アプリケーションの構成」で整理します。
1.3 事前準備
前提となる講座
この章では、以下の知識を前提としています。自信がない場合は先に関連講座を実施してみましょう。
| 講座名 | 必要な知識 |
|---|---|
| Webアプリケーションの基本 | HTTPリクエスト・レスポンスやWebアプリケーションの基本構造 |
| SQL基本文法 | SELECT・INSERT・UPDATE・DELETEなど、SQLの基本操作 |
必要なツール
この章では、以下のツールを使用します。まだインストールしていない場合は、リンク先の手順に沿って準備をお願いします。
| ツール名 | 関連箇所 | 理由 |
|---|---|---|
| Visual Studio Code | Visual Studio Codeのインストール | REST APIのコードを記述するエディタとして使用する |
| Go | Goのインストール | REST APIの実装・実行環境として使用する |
| MySQL | MySQLのインストール | REST APIから接続するデータベースとして使用する |
2. データベースの準備
APIの実装に入る前に、接続先となるデータベースを先に用意します。テーブルは後の実装で自動的に作られるため、この段階ではデータベースだけを作成します。
まず、root ユーザでMySQLにログインします。
mysql -u root -p
パスワードを入力してMySQLのプロンプトに切り替わったら、以下のSQLを実行してデータベースを作成します。
CREATE DATABASE notes_api;
以下のような実行結果が表示されます。
Query OK, 1 row affected (0.00 sec)
Query OK が返れば、notes_api データベースが作成できています。作成が確認できたら、exit; でMySQLのプロンプトを抜けます。
exit;
3. アプリケーションの構成
3.1 使用するライブラリ
Gin は Gin入門 で学んだ通り、ハンドラの書き方をシンプルにし、リクエストの取り出しやレスポンス書き込みを直感的に書けるようにするWebフレームワークです。GORM は GORM入門 で学んだ通り、モデルベースのCRUDをコンパクトに書けるORMです。本章では、Gin と GORM を組み合わせてノートアプリケーションのREST APIを実装します。
3.2 実装するCRUD操作
ノートに対して以下の5つのCRUD操作を扱います。この Create / Index / Show / Update / Delete という構成は、単一リソース(ここでは「ノート」)を扱うREST APIで一般的に用いられるパターンで、Ruby on Rails・Laravel・FastAPI など多くのWebフレームワークでも同じ組み合わせが採用されています。本章では、Create → Index → Show → Update → Delete の順で1節ずつ実装していきます。
| 操作 | メソッド | パス | 内容 |
|---|---|---|---|
| Create | POST | /notes |
ノートを新規作成する |
| Index | GET | /notes |
ノートの一覧を取得する |
| Show | GET | /notes/{id} |
指定IDのノートを1件取得する |
| Update | PUT | /notes/{id} |
指定IDのノートを更新する |
| Delete | DELETE | /notes/{id} |
指定IDのノートを削除する |
3.3 ファイル構成
CRUDを実装するにあたって、以下の3ファイル構成で進めます。各ファイルは同じパッケージ(package main)に属し、役割ごとに分けます。
notes-gin/
├── main.go ← エントリポイント(DB接続・ルーティング・サーバ起動)
├── handler.go ← 各エンドポイントのハンドラ関数
└── model.go ← Noteモデル(DBテーブルとGoの構造体の対応付け)
各ファイルの役割は以下のとおりです。
| ファイル | 役割 |
|---|---|
model.go |
データベーステーブルと対応するGoの構造体(Note)を定義する |
handler.go |
各HTTPエンドポイントの処理を担うハンドラ関数を定義する |
main.go |
データベース接続、ルーティング登録、サーバ起動などのエントリポイント処理を担う |
以降は、プロジェクトの準備 → モデル定義 → main.go の初期実装 → 各CRUDハンドラ(作成 → 一覧取得 → 単一取得 → 更新 → 削除)の順で、少しずつファイルに追記しながら進めます。
4. APIの実装
4.1 プロジェクトの準備
任意の場所に notes-gin フォルダを作成し、Visual Studio Codeの「ファイル」→「フォルダーを開く」から作成した notes-gin フォルダを開きます。以降の操作は、Visual Studio Codeのターミナルから行います。
notes-gin/ ← このフォルダを作成
モジュールを初期化します。
go mod init notes-gin
以下のような実行結果が表示されます。
go: creating new go.mod: module notes-gin
続いて、Gin本体を導入します。
go get github.com/gin-gonic/gin
以下のような実行結果が表示されます(バージョンは環境により変わります)。
go: added github.com/gin-gonic/gin v1.10.0
GORM本体を導入します。
go get gorm.io/gorm
以下のような実行結果が表示されます(バージョンは環境により変わります)。
go: added gorm.io/gorm v1.25.12
続いて、GORMのMySQL用ドライバを導入します。
go get gorm.io/driver/mysql
以下のような実行結果が表示されます(バージョンは環境により変わります)。
go: added gorm.io/driver/mysql v1.5.7
最後に、.env ファイルから接続情報を読み込むための godotenv パッケージを導入します。DB接続用のユーザ名やパスワードといった秘匿情報をコードに直接書かず、.env ファイルから環境変数として読み込むために使います。
go get github.com/joho/godotenv
以下のような実行結果が表示されます(バージョンは環境により変わります)。
go: added github.com/joho/godotenv v1.5.1
go.mod に4つの依存関係が並び、go.sum が生成されていれば準備完了です。中身を確認すると、require ディレクティブに導入した4つのパッケージと、それらが内部で使う間接的な依存パッケージが並んでいます。
module notes-gin
go 1.x
require (
github.com/gin-gonic/gin v1.x.x // indirect
github.com/joho/godotenv v1.x.x // indirect
gorm.io/driver/mysql v1.x.x // indirect
gorm.io/gorm v1.x.x // indirect
// ... 上記に加えて、Gin と GORM が内部で使う複数の間接依存パッケージも並びます
)
この時点ではまだ main.go を書いていないため、go get で導入したパッケージも含めて全て // indirect(間接的な依存関係)としてマークされています。次のステップで main.go に import を書くと、直接使うパッケージからは // indirect が外れます。
4.2 モデルの定義
最初に、ノートを表す Note モデルを定義します。この後に作る main.go(db.AutoMigrate(&Note{}) でテーブルを自動作成)や handler.go(各ハンドラ関数)が Note 構造体を参照するため、参照される側の型を先に用意しておく必要があります。DBの notes テーブルの1行が、Goの Note 構造体1つに対応します。
本章で扱う notes テーブルは、以下のカラム構成で作ります。
| カラム名 | 型 | 制約 | 説明 |
|---|---|---|---|
id |
bigint unsigned | PRIMARY KEY / AUTO_INCREMENT | ノートを一意に識別するID |
title |
varchar(200) | NOT NULL | ノートのタイトル |
body |
text | - | ノート本文 |
created_at |
datetime | - | ノートを作成した日時(GORMが自動で書き込む) |
このテーブル定義は、次に定義する Note 構造体の gorm タグから GORM の AutoMigrate によって自動生成されます。
Visual Studio Codeのエクスプローラーで notes-gin フォルダを右クリックし、「新しいファイル」を選択して model.go を作成します。
notes-gin/
├── go.mod
├── go.sum
└── model.go ← このファイルを作成
ノートの型を宣言するため、作成した model.go に以下の内容を記述して保存します。
package main
import "time"
type Note struct {
ID uint `gorm:"primaryKey" json:"id"`
Title string `gorm:"size:200;not null" json:"title" binding:"required"`
Body string `gorm:"type:text" json:"body"`
CreatedAt time.Time `json:"created_at"`
}
コードを解説します。
type Note struct {
ID uint `gorm:"primaryKey" json:"id"`
Title string `gorm:"size:200;not null" json:"title" binding:"required"`
Body string `gorm:"type:text" json:"body"`
CreatedAt time.Time `json:"created_at"`
}
gorm タグはGORMが notes テーブルを作成する際の制約を、json タグはJSONレスポンスに含めるキー名を指定します。Title の binding:"required" は、後で扱う c.ShouldBindJSON で必須フィールドとして扱うためのタグです。CreatedAt はGORMがレコード作成時刻を自動で書き込む特別なフィールド名です。
4.3 .env ファイルの作成
main.go の中にDB接続用のユーザ名・パスワードを直接書くと、コードを変更するたびに書き換える必要があり、うっかりGitに含めてしまうリスクもあります。ここでは、接続情報を .env ファイルにまとめて、Goのコードからは環境変数として読み込む形にします。
Visual Studio Codeのエクスプローラーで notes-gin フォルダを右クリックし、「新しいファイル」を選択して .env ファイルを作成します。
notes-gin/
├── .env ← このファイルを作成
├── go.mod
├── go.sum
└── model.go
作成した .env ファイルに以下の内容を記述して保存します。各項目の意味は下記のとおりです。
| 変数名 | 説明 |
|---|---|
MYSQL_USER |
接続先MySQLのユーザ名を指定する。ここでは root を使う |
MYSQL_PASSWORD |
上記ユーザのパスワードを指定する。先ほど mysql -u root -p で入力した自分のMySQLパスワードに書き換える |
MYSQL_HOST |
接続先MySQLのホスト名またはIPアドレスを指定する。ローカルで動作しているMySQLに接続するため 127.0.0.1 を使う |
MYSQL_PORT |
接続先MySQLの待ち受けポートを指定する。MySQLのデフォルトである 3306 を使う |
MYSQL_DATABASE |
接続先のデータベース名を指定する。先ほど作成した notes_api を指定する |
MYSQL_USER=root
MYSQL_PASSWORD=あなたのパスワード
MYSQL_HOST=127.0.0.1
MYSQL_PORT=3306
MYSQL_DATABASE=notes_api
4.4 main.go の初期実装
続いて、エントリポイントとなる main.go を作成します。まずはデータベース接続と Note テーブルの自動作成、サーバ起動までを実装します。エンドポイントは以降の節で少しずつ追加していきます。
main.go の作成
notes-gin フォルダに main.go を作成します。
notes-gin/
├── .env
├── go.mod
├── go.sum
├── main.go ← このファイルを作成
└── model.go
DB接続とサーバ起動をまとめて実装するため、作成した main.go に以下の内容を記述して保存します。
package main
import (
"fmt"
"os"
"github.com/gin-gonic/gin"
"github.com/joho/godotenv"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
var db *gorm.DB
func main() {
if err := godotenv.Load(); err != nil {
fmt.Println(".envファイル読み込み失敗:", err)
return
}
dsn := fmt.Sprintf(
"%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
os.Getenv("MYSQL_USER"),
os.Getenv("MYSQL_PASSWORD"),
os.Getenv("MYSQL_HOST"),
os.Getenv("MYSQL_PORT"),
os.Getenv("MYSQL_DATABASE"),
)
var err error
db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic(err)
}
db.AutoMigrate(&Note{})
r := gin.Default()
r.Run(":8080")
}
サーバの起動確認
以下のコマンドでサーバを起動します。go run . は、カレントディレクトリのGoパッケージ全体(main.go と model.go)をまとめてコンパイルして実行します。go run main.go のようにファイル名を指定してしまうと model.go の Note 型が読み込まれず、コンパイルエラーになります。
go run .
以下のような実行結果が表示されます。
[GIN-debug] Listening and serving HTTP on :8080
Gin独自のdebugログとともにポート 8080 で待ち受け状態になれば、DB接続と notes テーブルの自動作成、サーバ起動が正しく動いています。エンドポイントはまだ登録していないため、curl を叩いても 404 が返りますが、以降の節で1つずつルートを追加していきます。
| ⚠️ 「./main.go:XX:XX: undefined: Note」というエラーが出る場合 |
|---|
go run main.go のようにファイル名を指定して実行している可能性が高いです。本章はマルチファイル構成(main.go と model.go)で、main.go から model.go の Note 型を参照しているため、単一ファイル指定ではコンパイルが通りません。go run . としてカレントディレクトリのGoパッケージ全体をまとめてコンパイル・実行してください。 |
| ⚠️ 「panic: Error 1045 (28000): Access denied for user 'root'@...」というエラーが出る場合 |
|---|
.env の MYSQL_PASSWORD が実際のMySQLパスワードと一致していない可能性が高いです。.env を、mysql -u root -p でログインできたパスワードに書き換えてから、再度 go run . を実行してください。 |
作成されたテーブルの確認
AutoMigrate(&Note{}) によって、この起動のタイミングで notes テーブルが自動作成されています。MySQL CLIから実際にテーブルが作られていることを確認します。別のターミナルを開き、root ユーザでMySQLにログインします。
mysql -u root -p
ログイン後、notes_api データベースを使用します。
USE notes_api;
続いて、テーブル一覧を表示します。
SHOW TABLES;
以下のように、notes テーブルが一覧に表示されます。
+---------------------+
| Tables_in_notes_api |
+---------------------+
| notes |
+---------------------+
続いて、notes テーブルの構造を確認します。
DESCRIBE notes;
以下のように、Note 構造体で定義したフィールドがそのままカラムとして作られています。
+------------+-----------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+-----------------+------+-----+---------+----------------+
| id | bigint unsigned | NO | PRI | NULL | auto_increment |
| title | varchar(200) | NO | | NULL | |
| body | text | YES | | NULL | |
| created_at | datetime(3) | YES | | NULL | |
+------------+-----------------+------+-----+---------+----------------+
Goで書いた Note 構造体の定義が、MySQL側のテーブル構造にそのまま反映されていることが確認できました。確認できたら、exit; でMySQLのプロンプトを抜けます。
exit;
コードの解説
コードを解説します。
var db *gorm.DB
db をパッケージレベルの変数として宣言します。この後の節で追加するハンドラ関数からも同じDB接続を参照できるようにします。
if err := godotenv.Load(); err != nil {
fmt.Println(".envファイル読み込み失敗:", err)
return
}
godotenv.Load() で、カレントディレクトリの .env を読み込んで環境変数として登録します。以降の os.Getenv("MYSQL_USER") などで各値を取り出せるようになります。
dsn := fmt.Sprintf(
"%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
os.Getenv("MYSQL_USER"),
os.Getenv("MYSQL_PASSWORD"),
os.Getenv("MYSQL_HOST"),
os.Getenv("MYSQL_PORT"),
os.Getenv("MYSQL_DATABASE"),
)
.env から読み込んだ環境変数を組み合わせて、MySQLへの接続文字列(DSN)を作ります。パスワードなどの秘匿情報を直接コードに書かないため、Gitに含めるファイル(main.go)と含めないファイル(.env)を分けて管理できます。
db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
...
db.AutoMigrate(&Note{})
gorm.Open でMySQLへ接続し、AutoMigrate で Note モデルから notes テーブルを自動生成します。テーブルが既に存在する場合は不足カラムだけを追加します。
r := gin.Default()
...
r.Run(":8080")
Ginのルータを初期化し、ポート 8080 で待ち受けを開始します。この段階では r.GET / r.POST などのルート登録はまだありません。
5. CRUD処理の実装
ここからは、5つのCRUD処理を1つずつ実装していきます。各節では、handler.go にハンドラ関数を追加し、main.go に対応するルートを1行追加してから、curl で動作を確認する流れで進めます。実装順序は Create → Index → Show → Update → Delete です。
5.1 ノートの作成(Create)
最初のエンドポイントとして、ノートを新規作成する POST /notes を追加します。
実装
notes-gin フォルダに handler.go を作成します。
notes-gin/
├── go.mod
├── go.sum
├── handler.go ← このファイルを作成
├── main.go
└── model.go
ノート作成のハンドラを定義するため、作成した handler.go に以下の内容を記述して保存します。
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func createNote(c *gin.Context) {
var input Note
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := db.Create(&input).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, input)
}
続いて、POST /notes を createNote に紐づけるため、main.go の r := gin.Default() の下、r.Run(":8080") の上に、以下の1行を追加してルートを登録します。
r.POST("/notes", createNote)
動作確認
既存のサーバが動いている場合は Ctrl+C で停止し、以下のコマンドで再起動します。
go run .
別のターミナルで、ノートを3つ作成します。この後の Index(一覧取得)と Show(単一取得)の違いを比較しやすくするため、複数件を先に登録しておきます。まず1件目です。
curl -X POST http://localhost:8080/notes -H "Content-Type: application/json" -d '{"title":"買い物","body":"卵とパン"}'
以下のような実行結果が表示されます(id と created_at は環境により変わります)。
{"id":1,"title":"買い物","body":"卵とパン","created_at":"2026-07-15T12:00:00+09:00"}
続いて2件目です。
curl -X POST http://localhost:8080/notes -H "Content-Type: application/json" -d '{"title":"会議メモ","body":"9時から進捗共有"}'
以下のような実行結果が表示されます。
{"id":2,"title":"会議メモ","body":"9時から進捗共有","created_at":"2026-07-15T12:00:01+09:00"}
最後に3件目です。
curl -X POST http://localhost:8080/notes -H "Content-Type: application/json" -d '{"title":"読書メモ","body":"Go言語入門書"}'
以下のような実行結果が表示されます。
{"id":3,"title":"読書メモ","body":"Go言語入門書","created_at":"2026-07-15T12:00:02+09:00"}
POST /notes にJSONを送信するたびに、DBに保存されて、自動採番された id と作成日時 created_at を含むレスポンスが返りました。この後の Index / Show の節では、この3件を使って一覧取得と単一取得の違いを確認していきます。
| ⚠️ 「404 page not found」が返る場合 |
|---|
main.go に r.POST("/notes", createNote) の1行が追加されていないか、func main() { ... } の外側に書かれている可能性があります。r := gin.Default() の下、r.Run(":8080") の上に書かれていることを確認し、保存後に Ctrl+C で既存サーバを止めて go run . で再起動してください。以降の CRUD 節でも同じチェックが有効です。 |
コードの解説
var input Note
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.ShouldBindJSON(&input) で、リクエストボディのJSONを Note 構造体にデコードすると同時に、binding:"required" タグに基づくバリデーションを実行します。失敗した場合は 400 Bad Request を返して早期リターンします。
if err := db.Create(&input).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, input)
db.Create(&input) でDBに新規レコードを挿入します。GORMは自動採番された ID と CreatedAt を input に書き戻すため、c.JSON(http.StatusCreated, input) で id と created_at を含むJSONをそのまま返せます。ステータスコードは、リソース作成時の慣例的な 201 Created を返します。
5.2 ノート一覧の取得(Index)
続いて、ノートの一覧を取得する GET /notes を追加します。
ノート一覧を取得するハンドラを追加するため、handler.go の末尾に、以下の関数を追記します。
func listNotes(c *gin.Context) {
var notes []Note
if err := db.Order("id DESC").Find(¬es).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, notes)
}
続いて、GET /notes を listNotes に紐づけるため、main.go の r.POST("/notes", createNote) の下に、以下の1行を追加します。
r.GET("/notes", listNotes)
既存のサーバが動いている場合は Ctrl+C で停止し、以下のコマンドで再起動します。
go run .
別のターミナルで、一覧を取得します。
curl http://localhost:8080/notes
以下のような実行結果が表示されます。3件のノートが id の降順(新しい順)で並んで返ります。
[{"id":3,"title":"読書メモ","body":"Go言語入門書","created_at":"2026-07-15T12:00:02+09:00"},{"id":2,"title":"会議メモ","body":"9時から進捗共有","created_at":"2026-07-15T12:00:01+09:00"},{"id":1,"title":"買い物","body":"卵とパン","created_at":"2026-07-15T12:00:00+09:00"}]
GET /notes で、先ほど作成した3件のノートが JSON 配列として返ってきました。ハンドラで Order("id DESC") を指定しているため、新しく作成された id = 3 が先頭に来ています。
コードを解説します。
var notes []Note
if err := db.Order("id DESC").Find(¬es).Error; err != nil {
...
}
c.JSON(http.StatusOK, notes)
db.Find(¬es) で notes テーブル全件を []Note に読み込みます。Order("id DESC") を挟むことで、新しく作成されたものから順に並びます。取得したスライスをそのまま c.JSON に渡すと、GORMが読み込んだ全レコードがJSON配列として返ります。
5.3 ノートの単一取得(Show)
続いて、指定IDのノートを1件だけ取得する GET /notes/:id を追加します。存在しないIDが指定された場合は 404 Not Found を返します。
実装
handler.go の import に "errors" と "gorm.io/gorm" を追加します。以降の節で gorm.ErrRecordNotFound を扱うためです。
import (
"errors"
"net/http"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
続いて、指定IDのノートを取得するハンドラを追加するため、handler.go の末尾に、以下の関数を追記します。
func getNote(c *gin.Context) {
var n Note
if err := db.First(&n, 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": err.Error()})
return
}
c.JSON(http.StatusOK, n)
}
続いて、GET /notes/:id を getNote に紐づけるため、main.go の r.GET("/notes", listNotes) の下に、以下の1行を追加します。
r.GET("/notes/:id", getNote)
動作確認
既存のサーバが動いている場合は Ctrl+C で停止し、以下のコマンドで再起動します。
go run .
別のターミナルで、id = 1 のノートを取得します。
curl http://localhost:8080/notes/1
以下のような実行結果が表示されます。
{"id":1,"title":"買い物","body":"卵とパン","created_at":"2026-07-15T12:00:00+09:00"}
GET /notes/1 により、パスパラメータで指定した id = 1 のノートが単一のJSONオブジェクトとして返りました。Index では3件が配列 [...] として返っていたのに対して、Show では指定した1件だけがオブジェクト {...} として返る点が違いです。
続いて、存在しない id を指定した場合の挙動も確認します。ハンドラで gorm.ErrRecordNotFound を判定して 404 Not Found を返す処理が入っているためです。
curl -i http://localhost:8080/notes/999
以下のような実行結果が表示されます。
HTTP/1.1 404 Not Found
Content-Type: application/json; charset=utf-8
Date: XXX, XX XXX XXXX XX:XX:XX GMT
Content-Length: 44
{"error":"ノートが見つかりません"}
存在しない id = 999 を指定したところ、ステータス 404 Not Found と、ハンドラで返している {"error":"ノートが見つかりません"} の JSON が返りました。-i オプションを付けることで、ボディだけでなくステータス行やヘッダも合わせて確認できます。
コードの解説
var n Note
if err := db.First(&n, c.Param("id")).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "ノートが見つかりません"})
return
}
...
}
db.First(&n, c.Param("id")) は主キーの値でレコードを1件取得します。見つからない場合は gorm.ErrRecordNotFound が返るので、errors.Is でそのエラーかどうかを判定し、404 Not Found を返します。それ以外のエラー(DB接続失敗など)は 500 Internal Server Error で返します。
5.4 ノートの更新(Update)
続いて、指定IDのノートを更新する PUT /notes/:id を追加します。
実装
ノートを更新するハンドラを追加するため、handler.go の末尾に、以下の関数を追記します。
func updateNote(c *gin.Context) {
var n Note
if err := db.First(&n, 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": err.Error()})
return
}
var input Note
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
n.Title = input.Title
n.Body = input.Body
if err := db.Save(&n).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, n)
}
続いて、PUT /notes/:id を updateNote に紐づけるため、main.go の r.GET("/notes/:id", getNote) の下に、以下の1行を追加します。
r.PUT("/notes/:id", updateNote)
動作確認
既存のサーバが動いている場合は Ctrl+C で停止し、以下のコマンドで再起動します。
go run .
別のターミナルで、id = 1 のノートを更新します。
curl -X PUT http://localhost:8080/notes/1 -H "Content-Type: application/json" -d '{"title":"買い物","body":"卵、パン、牛乳"}'
以下のような実行結果が表示されます。
{"id":1,"title":"買い物","body":"卵、パン、牛乳","created_at":"2026-07-15T12:00:00+09:00"}
id = 1 のノートの body が更新され、更新後の内容がJSONで返りました。
更新が実際にDBに反映されていることを、Show(GET /notes/:id)で id = 1 のノートを取り出して確認します。
curl http://localhost:8080/notes/1
以下のような実行結果が表示されます。
{"id":1,"title":"買い物","body":"卵、パン、牛乳","created_at":"2026-07-15T12:00:00+09:00"}
body が 卵、パン、牛乳 に更新された状態で返ってきました。PUT で送った内容がDBに保存され、その後の GET でも同じ内容が読み取れることが確認できています。
続いて、存在しない id を指定した場合の挙動も確認します。ハンドラで gorm.ErrRecordNotFound を判定して 404 Not Found を返す処理が入っているためです。
curl -X PUT http://localhost:8080/notes/999 -H "Content-Type: application/json" -d '{"title":"任意","body":"任意"}' -i
以下のような実行結果が表示されます。
HTTP/1.1 404 Not Found
Content-Type: application/json; charset=utf-8
Date: XXX, XX XXX XXXX XX:XX:XX GMT
Content-Length: 44
{"error":"ノートが見つかりません"}
存在しない id = 999 を指定したところ、Show と同じく 404 Not Found と {"error":"ノートが見つかりません"} の JSON が返りました。
コードの解説
var n Note
if err := db.First(&n, c.Param("id")).Error; err != nil { ... }
var input Note
if err := c.ShouldBindJSON(&input); err != nil { ... }
n.Title = input.Title
n.Body = input.Body
if err := db.Save(&n).Error; err != nil { ... }
まず db.First で既存レコードを取り出し、次に c.ShouldBindJSON でリクエストボディを別の構造体に読み込みます。既存レコードのフィールドを書き換えてから db.Save(&n) を呼ぶことで、updated_at などの管理カラムも含めて安全に更新できます。
5.5 ノートの削除(Delete)
最後に、指定IDのノートを削除する DELETE /notes/:id を追加します。
ノートを削除するハンドラを追加するため、handler.go の末尾に、以下の関数を追記します。
func deleteNote(c *gin.Context) {
if err := db.Delete(&Note{}, c.Param("id")).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.Status(http.StatusNoContent)
}
続いて、DELETE /notes/:id を deleteNote に紐づけるため、main.go の r.PUT("/notes/:id", updateNote) の下に、以下の1行を追加します。
r.DELETE("/notes/:id", deleteNote)
既存のサーバが動いている場合は Ctrl+C で停止し、以下のコマンドで再起動します。
go run .
別のターミナルで、id = 1 のノートを削除します。
curl -X DELETE http://localhost:8080/notes/1 -i
以下のような実行結果が表示されます(204 No Content が返るのでボディはありません)。
HTTP/1.1 204 No Content
...
削除が成功したため、ステータス 204 No Content(ボディ無し)が返りました。-i オプションを付けているので、ステータス行とヘッダが表示されています。
コードを解説します。
if err := db.Delete(&Note{}, c.Param("id")).Error; err != nil { ... }
c.Status(http.StatusNoContent)
db.Delete(&Note{}, c.Param("id")) で、パスパラメータで受け取った id のレコードを削除します。成功時は c.Status(http.StatusNoContent) で 204 No Content を返し、レスポンスボディを持たないことを示します。
これで、5つのCRUDエンドポイントを持つノートアプリケーションのREST APIが完成しました。
6. 不要リソースの削除
ハンズオンで使った notes_api データベースを残しておく必要はないため、削除します。まず、root ユーザでMySQLにログインします。
mysql -u root -p
パスワードを入力してMySQLのプロンプトに切り替わったら、以下のSQLを実行してデータベースを削除します。
DROP DATABASE notes_api;
以下のような実行結果が表示されます。
Query OK, 0 rows affected (0.01 sec)
Query OK が返れば削除完了です。削除が確認できたら、exit; でMySQLのプロンプトを抜けます。
exit;
7. まとめ
この章では、Gin と GORM を組み合わせて、ノートアプリケーションのREST APIを実装する流れを体験しました。
.envファイルにDB接続情報(MYSQL_USER/MYSQL_PASSWORDなど)を外に出し、godotenvで環境変数として読み込むことで、パスワードなどの秘匿情報をコードから分離できる- Gin を使うと、ルーティング(
r.GET("/notes", listNotes))・パスパラメータ取得(c.Param("id"))・JSONレスポンス書き出し(c.JSON(status, body))をそれぞれ1行前後で書ける - リクエストのJSONバインドとバリデーションを
c.ShouldBindJSON(&input)にまとめ、struct タグのbinding:"required"で必須チェックまで自動化できる - GORM の
First/Find/Create/Save/Deleteにより、CRUD操作を struct のメソッド呼び出しで書ける - Gin と GORM の組み合わせで、5つのCRUDエンドポイントを持つREST APIをコンパクトに実装できる
次の章では、GoでCORS対応を学びつつ、実際にブラウザからの呼び出しをGinで許可する実装を体験します。