0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Goを使った RESTful API Todoバックエンドの実装

Last updated at Posted at 2025-03-25

あいさつ

こんにちは.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の利点をほぼ活かしていません.

今後並行処理を実装するときに役立つことでしょう.

動作確認方法

セットアップ手順

  1. リポジトリをクローン

    git clone https://github.com/gs223gs/go-api-todo
    
  2. ディレクトリに移動

    cd go-api-todo
    
  3. コンテナを起動

    docker compose up -d
    docker compose exec go bash
    
  4. go.modを作成

    cd 使いたいAPIに移動 => 例: v1/rest
    go mod init github.com/gs223gs/go-api-todo
    go mod tidy
    
  5. APIを起動

    go run main.go
    
  6. お好きなAPIにアクセス

    • JSONでやりとりします。

簡単なシステム構成図

image.png

主な機能一覧

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等もご気軽にください
嬉しいです.

コードレビュー大歓迎です.

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?