あいさつ
こんにちは.25歳 25専門卒 T.Miuraです.来週から新卒として働きます.2度目の新卒です.
今回作成しているものが
一区切りついた + そろそろ次の学習に進めたい
ため,記事にします
学習の背景
最近Golangに手を出し始めました.
まだまだ,Goの良さを引き出せずにいるので更なる研鑽を積んでいきます.
言語だけではなく,APIについても学習を進めています.
本記事について
学んだことを活かして RESTに基づいたAPI を実装したこと
実装に伴って学んだこと,悩んだこと,気がついたこと
をまとめています
はじめに
なぜ作ろうと考えたのか?
フロントエンドの新技術, FWで活用するためです.
フロントエンドの発展は目まぐるしいですよね.
言語はJSですが,FWはちょっと目を離した隙に進化や新陳代謝を繰り返しています.
新たなものを学ぶときにTodoサイトを構築するのはよくある話です.
CRUDは基礎の基礎ですからね.
しかし,私はバックエンドを構築せずにモックや簡易的なもので済ませていました.
例えば,React入門の際はstateに全てのTodoを持たせました.
触り程度であればそれでもいいのですが,どこでAPIを叩くか?どうやってresponseを受け取るか?
そういった技術や仕組みは身につけれずにいました.
それではもったいないことこの上ない.
実際にAPIを叩けるバックエンドを作成し使いまわしてやる!!!
これが作ろうとした理由です.
作成したものについて
使用技術スタック
- Docker
- Go/gin
- postgres
私はPCに何も入れたくないのでDocker上でLinux, 今回はUbuntuのコンテナを立ててその中でGoを使用し作業しています.
なお,Goを使用していますが,今回のTodoバックエンドでは,言語はなんでもいいと思います.
現時点では,Goの利点をほぼ活かしていません.
今後並行処理を実装するときに役立つことでしょう.
動作確認方法
セットアップ手順
-
リポジトリをクローン
git clone https://github.com/gs223gs/go-api-todo
-
ディレクトリに移動
cd go-api-todo
-
コンテナを起動
docker compose up -d docker compose exec go bash
-
go.mod
を作成cd 使いたいAPIに移動 => 例: v1/rest go mod init github.com/gs223gs/go-api-todo go mod tidy
-
APIを起動
go run main.go
-
お好きなAPIにアクセス
- JSONでやりとりします。
簡単なシステム構成図
主な機能一覧
Todo & カテゴリ 取得, 登録, 更新, 削除
APIについて
Todo API (v1/rest/todo)
メソッド | エンドポイント | 説明 |
---|---|---|
GET | /v1/rest/todo | Todo一覧の取得 |
POST | /v1/rest/todo | 新規Todo作成 |
PUT | /v1/rest/todo | 指定したIDのTodo更新 |
DELETE | /v1/rest/todo | 指定したIDのTodo削除 |
Category API (v1/rest/category)
メソッド | エンドポイント | 説明 |
---|---|---|
GET | /v1/rest/category | カテゴリ一覧の取得 |
POST | /v1/rest/category | 新規カテゴリ作成 |
PUT | /v1/rest/category | 指定したIDのカテゴリ更新 |
DELETE | /v1/rest/category | 指定したIDのカテゴリ削除 |
コード・リクエスト/レスポンスの一部抜粋
Todo API
GET /v1/rest/todo
r.GET("/v1/rest/todo", func(c *gin.Context) {
var todos []structs.Todos
db.Select("Id", "Title", "Content", "Category_Id", "Due", "Is_Done", "Created_at", "Updated_at").Find(&todos)
var todosResponse []structs.TodosResponse
for _, todo := range todos {
todosResponse = append(todosResponse, structs.TodosResponse{
Id: todo.Id,
Title: todo.Title,
Content: todo.Content,
Category_id: todo.Category_Id,
Is_Done: todo.Is_Done,
Due: func() string {
if todo.Due != nil {
return todo.Due.Format(time.RFC3339)
}
return ""
}(),
Created_at: todo.Created_at.Format(time.RFC3339),
Updated_at: todo.Updated_at.Format(time.RFC3339),
})
}
c.JSON(http.StatusOK, todosResponse)
})
レスポンス
{
"id": 1,
"title": "買い物に行く",
"content": "牛乳と卵を買う",
"category_id": 1,
"is_done":false,
"due": "2024-03-20T15:00:00Z",
"created_at": "2024-03-19T10:00:00Z",
"updated_at": "2024-03-19T10:00:00Z"
},
POST /v1/rest/todo
r.POST("/v1/rest/todo", func(c *gin.Context) {
var todo structs.Todos
if err := c.ShouldBindJSON(&todo); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var validate = map[string]any{"TodoTitle": todo.Title, "CategoryID": todo.Category_Id}
if err := validation.Check(validate, db); len(err) != 0 {
c.JSON(http.StatusBadRequest, gin.H(validation.Conv(err)))
return
}
if err := db.Create(&todo).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "登録に失敗しました"})
return
}
c.JSON(http.StatusOK, gin.H{"messege": "追加完了"})
})
リクエスト
{
"title": "買い物に行く",
"content": "牛乳と卵を買う",
"category_id": 1,
"due": "2024-03-20T15:00:00Z", // なくてもいい defaultは空文字
"is_done":true //なくてもいい defaultはfalse
}
レスポンス
{
"messege":"追加完了"
}
Category API
GET /v1/rest/category
r.GET("/v1/rest/category", func(c *gin.Context) {
var Categories []structs.Categories
db.Select("Id", "Category").Find(&Categories)
c.JSON(http.StatusOK, Categories)
})
レスポンス
{
{
"id": 1,
"category": "買い物"
},
{
"id": 2,
"category": "仕事"
}
}
ステータスコード
ステータスコード | 説明 |
---|---|
200 | 成功 |
400 | リクエスト不正 |
404 | リソースが見つからない |
500 | サーバーエラー |
データベース設計
Todos テーブル
カラム名 | 型 | 制約 | 説明 |
---|---|---|---|
id | SERIAL | PRIMARY KEY, AUTO INCREMENT | Todo項目の一意識別子 |
title | VARCHAR(255) | - | Todoのタイトル |
content | TEXT | - | Todoの詳細内容 |
category_id | INTEGER | FOREIGN KEY | カテゴリーの外部キー |
is_done | BOOLEAN | DEFAULT false | 完了フラグ |
due | TIMESTAMP | DEFAULT NULL | 期限 |
created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 作成日時 |
updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 更新日時 |
Categories テーブル
カラム名 | 型 | 制約 | 説明 |
---|---|---|---|
id | SERIAL | PRIMARY KEY, AUTO INCREMENT | カテゴリーの一意識別子 |
category | VARCHAR(255) | - | カテゴリー名 |
データモデルの説明
- TodoとCategoryは1対多の関係
- 1つのTodoは1つのカテゴリーに属する
- 1つのカテゴリーは複数のTodoを持つことができる
カテゴリを削除した場合について
削除したカテゴリを使用しているTodoも消えるようにしました.
整合性を取るためです.
エラーハンドリング
リクエストの中でTodo / Category IDの存在確認
Todo Title / Categoy(名) が空文字ではないかどうか
をチェックしてエラーメッセージをresponseで返すようにしています.
開発で苦労した点・工夫した点
バリデーションチェックに苦労しました.
エンドポイント毎にバリデーションチェックをしなければいけない.
無駄なコード量が多くなる問題が発生しました.
解決方法
バリデーション専用のパッケージを作成しました.
validate.Check(バリデーションチェックしたい項目: 値)
とすることで,nilかエラー文が入ったmapが帰ってきます.
エラー時には帰ってきたエラー文をginでJSONに変換し,responseとしています.
~追記~
エラー文をmapで返す機能を実装し忘れていました.
バリデーションチェックと言っても文字数や不正な文字のチェックはまだ実装していません.
次の認証/認可の際に実装します.
なお,バリデーションチェックパッケージにも,問題があると考えているので後述します
学んだこと
packageのわけ方
パッケージ/ファイルを分けることによってかなり管理しやすくなりました.
今後はディレクトリ構成についても深掘りしていきます
REST
本で読んでいるだけのでは学びきれないことを得れました.
RESTはWebの入門書には必ず登場する言葉ですが,曖昧な理解をしていました.
実装してみることでわかることが多かったです.
単一責任について
今のコードを単一責任に基づいてリファクタリングします.
テストのしやすさ,変更のしやすさがガラリと変わるはずです
ステータスコードの重要性
適切であれば,極論ステータスコードだけで全てがわかるのでは?と思うほど便利でした.
テストの偉大さ
テスト手法についての本は何度か読んだことがあるのですが,実践したことはありませんでした.
コードはAIに書いてもらいましたが,初めてのテストの実践で感動しました.
もうpostmanをいちいち手打ちする生活とはおさらばです.
しかし,問題も発生しました.
テストコードの更新が億劫で,途中からやめてしまいました.
本にも登場するテストのネックな所を肌で感じれてよかったです.
今後はTDDでの実装を目指します.
改善点
responseについて
JSONでresponseを返すまではいいのですが,JSONの内容に問題があったなと考えています.
errorや,GETしたTodoをひとつにまとめれるようにした方が絶対にいいです.
{
"todos": [
{
"id": 1,
"title": "買い物に行く",
"content": "牛乳と卵を買う",
// ... その他のTodoの内容
}
]
}
DBについて
DBのカラムに統一性がないなと感じています.
TodosではTodo名を title としています
Categoriesではカテゴリ名を category としています.
todo/category name とするか title
命名についてはいろいろな考え方があるのでなんでもいいとは思いますが,統一することが大事ですね.
登録処理について
現時点では,Todo/categoryの登録はバリデーションチェック後すぐに登録しています.
DAOのように,DBにアクセスする専用の物を用意するべきだったのではないか?と考えています.
今回はかなり小規模だったので,責任を分散させませんでした.
次回は分散させて実装してみたいと思います.
作るだけではなく,保守性も考えなければいけませんね
他人が読みやすいかどうか,修正しやすいかどうか?
vaidation packeageについて
読みやすさと言った面では validate パッケージは読みやすく,いい設計にしようと考えましたが,結果的に空回りに終わってしまいました.
package名.関数名と呼び出す特性上呼び出す時に
validation.Check()
としたのは何をしているのか読みやすいと考えました.
しかし,validation.Check()の作りには問題があったかなとも考えています.
引数として
"TodoID"や"TodoTitle"の文字列
バリデーションチェックしたい値
をKey-valueで渡しています
内部ではKeyをswitch文で分岐させ,処理に移るようにしています
一元管理するCheck()ではなくCheckID()やCheckTitle()のようにする方がわかりやすかったのではないか?
単一責任に反している.
Goの機能でもっと簡潔に描けるようになるのではないか?
と考えています.
次回はメソッドレシーバーあたりをさらに進めていきます.
まとめ
RESTは歴史も古く,まだまだ現役です.
このリポジトリを使えると考えた方はぜひ活用してください
また,PRやissue等もご気軽にください
嬉しいです.
コードレビュー大歓迎です.