LoginSignup
8
7

[golang]golangで簡単なCRUD操作を行うREST APIを作ってみた

Last updated at Posted at 2023-05-22

はじめに

golang で簡単な CRUD 操作を行う REST API を作ってみました
go, gin, gorm, mysql を使った、簡単な TODO アプリです

環境

PC Macbook Pro
OS Big Sur
docker-compose 1.24.1, build 4667896b
docker 19.03.4, build 9013bf5
go go1.20.4 linux/amd64
mysql 5.7

作ったもの

今回作った API の仕様は以下の通りです
簡単な TODO アプリで、 CRUD 操作の為のエンドポイントを提供します

Method Path Description Description
GET /todo 全件取得API TODO リストの全件取得
GET /todo/{id} 取得API 指定した TODO の取得
POST /todo 登録API TODO の作成
PUT /todo/{id} 更新API TODO の更新
DELETE /todo/{id} 削除API TODO の削除

ソースコードはこちら

ディレクトリ構成

ディレクトリ構成はこんな感じ
一応 Clean Achitectute を意識しているつもり

.
├── Dockerfile
├── README.md
├── cmd
│   └── go-api-sample-todo
│       └── main.go
├── docker-compose.yml
├── domain
│   ├── model
│   │   └── todo.go
│   └── repository
│       └── todo.go
├── go.mod
├── go.sum
├── handler
│   ├── todo.go
│   ├── todo_test.go
│   └── validator
│       └── validator.go
├── infrastructure
│   ├── db.go
│   ├── todo.go
│   └── todo_test.go
├── migrations
│   └── 0001_create_tables.sql
└── usecase
    ├── todo.go
    └── todo_test.go

各レイヤ・ディレクトリの役割

cmd

アプリのメインエントリが置かれるところ

handler

HTTP リクエストを受けとり usecase を呼び出し、処理結果を返す

usecase

ビジネスロジックを記述するところ
今回は特に複雑な処理はないので薄い実装になってます

domain

システムが扱う業務領域に関するコードを置くところ

repository

infrastructure 層を抽象化するための interface を定義

model

ドメインに関する値と振る舞いをもつところ

infrastructure

技術的な関心事を置くところ
今回だと、DB 操作周りを担っています

migrations

初期テーブルの作成や初期データの投入のためのSQLファイルを置くところ
今回使用するテーブルの DDL を配置しています
docker-compose.ymlに以下のような記述をすることで、初回起動時に/migrations内のSQLファイルを実行してくれます。初期テーブルの作成や初期データの投入に使うと便利です。
※初回コンテナ作成時にしか実行されないので、あとから DDL や DML を追加するときは、手動で実行するか、コンテナを作り直す必要があります

mysql:
      image: mysql:5.7
      container_name: mysql-db
      ports:
        - 3306:3306
      volumes:
        - ./migrations:/docker-entrypoint-initdb.d

コード

それでは実際のソースコードを順々に見ていきます

cmd/go-api-sample-todo/main.go

今回のソースのエントリポイントです
DB の設定と、API のルーティング設定(setupRouter)を行います
web フレームワークには gin を使っています
また、DB 操作のための ORM フレームワークには gorm を使っています

cmd/go-api-sample-todo/main.go
package main

import (
	"app/handler"
	"app/infrastructure"
	"app/usecase"
	"fmt"
	appvalidator "app/handler/validator"
	"github.com/gin-gonic/gin"
	"gorm.io/gorm"
)

func main() {
	d, err := infrastructure.NewDB()
	if err != nil {
		fmt.Printf("failed to start server. db setup failed, err = %s", err.Error())
		return
	}
	r := setupRouter(d)
	if err := appvalidator.SetupValidator(); err != nil {
		fmt.Printf("failed to start server. validator setup failed, err = %s", err.Error())
		return
	}
	r.Run()
}

func setupRouter(d *gorm.DB) *gin.Engine {
	r := gin.Default()

	repository := infrastructure.NewTodo(d)
	usecase := usecase.NewTodo(repository)
	handler := handler.NewTodo(usecase)

	todo := r.Group("/todo")
	{
		todo.POST("", handler.Create)
		todo.GET("", handler.FindAll)
		todo.GET("/:id", handler.Find)
		todo.PUT("/:id", handler.Update)
		todo.DELETE("/:id", handler.Delete)
	}
	return r
}

gin.Default()

gin.Default() は *gin.EngineをReturnする関数で、この Engine を使って、エンドポイントの設定やミドルウェアの登録を行うことができます
例えば、以下の様な記述をすることで、「pong」を返す/pingというエンドポイントを作ることができます

r := gin.Default()
    r.GET("/ping", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{
            "message": "pong",
        })
    })

今回の例では、更に r.Group("/todo")を使って、パスのグルーピングを行っています
こうすることで、共通のパスプレフィックスを持つエンドポイントを作ることができます
それぞれ実際には以下のようなエンドポイントになります

    // ルーティング設定
    todo := r.Group("/todo")
	{
        todo.POST("", handler.Create)       -> /todo
		todo.GET("", handler.FindAll)       -> /todo
		todo.GET("/:id", handler.Find)      -> /todo/:id
		todo.PUT("/:id", handler.Update)    -> /todo/:id
		todo.DELETE("/:id", handler.Delete) -> /todo/:id
	}

また、 todo.GET("", handler.FindAll)handler.FindAllは、
対象のエンドポイントにリクエストが来た時に呼び出す handler の関数を登録しています

登録API の実装

以下は、 登録 API の handler の実装です

handler/todo.go
// リクエストデータを格納するための構造体
type CreateRequestParam struct {
	Task string `json:"task" binding:"required,max=60"`
}

func (t *todoHandler) Create(c *gin.Context) {
	var req CreateRequestParam
    // リクエストパラメータを構造体(CreateRequestParam)にマッピング
	if err := c.ShouldBindJSON(&req); err != nil {
        // バリデーションエラーがあった場合はエラーを返す
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
    // usecase の呼び出し
	err := t.usecase.Create(req.Task)
	if err != nil {
        // エラーがあった場合はエラーレスポンスを返却
		c.JSON(http.StatusInternalServerError, "")
		return
	}
    // レスポンスを返却
	c.JSON(http.StatusCreated, nil)
}

CreateRequestParamで、リクエストを受け取るための構造体を定義し、
リクエストパラメータのタグ名の指定と「必須項目」、「桁数60桁まで」というバリデーション設定を行っています

type CreateRequestParam struct {
    // json:"task" = リクエストパラメータのタグ名の指定(指定しないと構造体名になってしまう)
    // required = 必須項目
    // max=60 = 桁数60桁まで
	Task string `json:"task" binding:"required,max=60"`
}

こうすることで、c.ShouldBindJSON(&req)で、構造体へのリクエストのパラメータのマッピングとバリデーションチェック行うことができます

    var req CreateRequestParam
    // リクエストパラメータを構造体(CreateRequestParam)にマッピング
	if err := c.ShouldBindJSON(&req); err != nil {
        // バリデーションエラーがあった場合はエラーを返す
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

バリデーションエラーになった場合の実際のレスポンスはこんな感じ

必須項目がない場合
 % curl -i localhost/todo -H "Content-Type: application/json" -X POST -d '{}'
HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
Date: Mon, 22 May 2023 16:37:26 GMT
Content-Length: 105

{"error":"Key: 'CreateRequestParam.Task' Error:Field validation for 'Task' failed on the 'required' tag"}
桁数オーバーの場合
% curl -i localhost/todo -H "Content-Type: application/json" -X POST -d '{"task": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}'
HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
Date: Mon, 22 May 2023 16:38:04 GMT
Content-Length: 100

{"error":"Key: 'CreateRequestParam.Task' Error:Field validation for 'Task' failed on the 'max' tag"

続いて、usecase 層では、handler から task を受け取り、 DB へデータの登録を行います

usecase/todo.go
func (t *todo) Create(task string) error {
	// 登録するための構造体を作成
    todo := model.NewTodo(task)
    // DB にデータを登録
	if err := t.todoRepository.Create(todo); err != nil {
		return err
	}
	return nil
}

todo := model.NewTodo(task)では、 domain/model/todo.goNewTodo(task string)を呼び出し、DB にデータを登録するための構造体を作成します

実装はこんな感じ

domain/model/todo.go
type Todo struct {
	ID        int `gorm:"primaryKey"`
	Task      string
	Status    TaskStatus
	CreatedAt time.Time `gorm:"<-:false"`
	UpdatedAt time.Time `gorm:"<-:false"`
}

func NewTodo(task string) *Todo {
	return &Todo{
		Task:   task,
		Status: Created,
	}
}

// Task Status の独自型の定義
type TaskStatus string

const (
	Created    = TaskStatus("created")
	Processing = TaskStatus("processing")
	Done       = TaskStatus("done")
)

NewTodo(task string) 関数は Todo の構造体を作成して返します
初回作成なので、Status は新規作成を表すCreated(created)を固定で入れます
また、 Statusには事前に定義した値のみを扱うために、以下のようにtype TaskStatus string で独自型を定義しています

// Task Status の独自型の定義
type TaskStatus string

const (
	Created    = TaskStatus("created")
	Processing = TaskStatus("processing")
	Done       = TaskStatus("done")
)

Todo の構造体は DB のテーブル 定義と合わせるようにしています
※本来レイヤ構造において、domain が他のレイヤに依存するような実装はだめなんだけど、今回はサンプルなので。。。

type Todo struct {
	ID        int `gorm:"primaryKey"`
	Task      string
	Status    TaskStatus
	CreatedAt time.Time `gorm:"<-:false"`
	UpdatedAt time.Time `gorm:"<-:false"`
}

gorm:xxx は gorm の用のタグで、テーブルの主キーの指定(gorm:"primaryKey")や、指定したカラムを書き込まないように(gorm:"<-:false")することができます

今回、created_atupdated_atは、テーブル定義として自動的に値が入るようにしているので、アプリケーション側からは書き込まないようにしています

テーブル定義はこんな感じ

migrations/0001_create_tables.sql
CREATE TABLE `todo` (
    `id` BIGINT(20) NOT NULL AUTO_INCREMENT comment 'ID',
    `task` VARCHAR (128) NOT NULL comment 'タスク',
    `status` VARCHAR(20) NOT NULL comment 'タスクステータス',
    `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP  comment '作成日時',
    `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP comment '更新日時',
PRIMARY KEY(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

最後に DB を操作する部分
t.todoRepository.Create(todo)では、repository を介して、infrastructure/todo.goCreate(t *model.Todo)を呼び出し、DB へのデータの登録を行います

infrastructure/todo.go
func (td *Todo) Create(t *model.Todo) error {
	if err := td.db.Create(t).Error; err != nil {
		return err
	}
	return nil
}

td.db.Create(t)では、 gorm の Create(value interface{})を呼び出しデータの登録を行っています

gorm には 基本的な CURD 操作の為の関数が用意されているので用途によって使い分けます
以下は一例

操作 関数
新規作成(Create) Create
参照(Read) Take, Find
更新(Update) Update
削除(Delete) Delete

※Update は 正確には upsert になります
主キーに該当するデータが有れば update、なければ insert
※参照用の関数に関しても、1件のみ取得であれば Take、複数件取得したい場合は Findを使うなど用途によって使い分けます

さて、これで登録API の実装完了したので、実際に API をコールしてデータが登録されるか見ていきたいと思います
まずは テーブル にデータが入っていないことを確認

mysql> select * from todo;
Empty set (0.00 sec)

はい、まだ空っぽです
それでは実際に API をコールしてデータの登録を行います

% curl -i localhost/todo -H "Content-Type: application/json" -X POST -d '{"task": "test"}'
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Date: Mon, 22 May 2023 16:07:23 GMT
Content-Length: 4

成功したっぽい
テーブルにデータが入っているか確認します

mysql> select * from todo;
+----+------+---------+---------------------+---------------------+
| id | task | status  | created_at          | updated_at          |
+----+------+---------+---------------------+---------------------+
|  1 | test | created | 2023-05-23 01:07:23 | 2023-05-23 01:07:23 |
+----+------+---------+---------------------+---------------------+
1 row in set (0.00 sec)

ちゃんとデータが登録されてます。よかった
これで登録API は完成です

更新API の実装

続いて、更新API の実装です
更新API では、 id を指定して対象データの task と status の更新を行うことができます
以下は更新API の handler の実装です

go:handler/todo.go
// パス(todo/:id)に指定されたパラメータを格納するための構造体
type UpdateRequestPathParam struct {
	ID int `uri:"id"`
}
// リクエストデータ(body)を格納するための構造体
type UpdateRequestBodyParam struct {
	Task   string           `json:"task" binding:"required,max=60"`
	Status model.TaskStatus `json:"status" binding:"required,task_status"`
}

func (t *todoHandler) Update(c *gin.Context) {
	var pathParam UpdateRequestPathParam
	var bodyParam UpdateRequestBodyParam
    // パスパラメータを構造体(UpdateRequestPathParam)にマッピング
	if err := c.ShouldBindUri(&pathParam); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
    // リクエストパラメータ(body)を構造体(UpdateRequestBodyParam)にマッピング
	if err := c.ShouldBindJSON(&bodyParam); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
    // usecase の呼び出し
	if err := t.usecase.Update(pathParam.ID, bodyParam.Task, bodyParam.Status); err != nil {
		c.JSON(http.StatusInternalServerError, "")
		return
	}
    // レスポンスを返却
	c.JSON(http.StatusNoContent, nil)
}

UpdateRequestPathParamUpdateRequestBodyParamでそれぞれパスパラメータとボディに指定されたリクエストパラメータを受け取る構造体を定義しています
UpdateRequestBodyParamでは、登録API と同様にバリデーション設定を行っているのですが、ここではtask_statusというカスタムバリデータを作り独自のバリデーションチェックを行うようにしています
これは、更新用API ではステータスの更新もできるのですが、事前定義されたステータスの値以外を受付ないようにするためです

カスタムバリデータの実装は以下のようになっています

handler/validator/validator.go
package validator

import (
	"app/domain/model"

	"github.com/gin-gonic/gin/binding"
	"github.com/go-playground/validator/v10"
)

func SetupValidator() error {
	if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
		if err := v.RegisterValidation("task_status", ValidateTaskStatus); err != nil {
			return err
		}
	}
	return nil
}
func ValidateTaskStatus(fl validator.FieldLevel) bool {
	return model.TaskStatusMap[model.TaskStatus(fl.Field().String())]
}

ValidateTaskStatus(fl validator.FieldLevel)が実際のバリデーションチェックを行う部分になっており、入力された値が事前定義された値に該当するかをチェックをし、 結果を bool で返します
チェックには、domain/model/todo.goに定義してある以下の関数を使用しています

var TaskStatusMap = map[TaskStatus]bool{
	Created:    true,
	Processing: true,
	Done:       true,
}

入力された値が TaskStatusMapに存在すれば trueを返し、存在しなければ falseを返します

SetupValidator()では作成したバリデーションチェックの関数の登録を行います

func SetupValidator() error {
	if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
        // 第一引数に任意のタグ名を指定、第二引数にバリデーションチェックに使用する関数を指定
		if err := v.RegisterValidation("task_status", ValidateTaskStatus); err != nil {
			return err
		}
	}
	return nil
}
func ValidateTaskStatus(fl validator.FieldLevel) bool {
	return model.TaskStatusMap[model.TaskStatus(fl.Field().String())]
}

続いて usecase 層
こちらも登録API と同様に handler から task の情報を受け取りデータの更新を行います

usecase/todo.go
func (t *todo) Update(id int, task string, status model.TaskStatus) error {
    // 更新用の構造体を生成
	todo := model.NewUpdateTodo(id, task, status)
    // DBのデータの更新
	if err := t.todoRepository.Update(todo); err != nil {
		return err
	}
	return nil
}

todo := model.NewUpdateTodo(id, task ,status)では、 domain/model/todo.goNewUpdateTodo(id int, task string, status TaskStatus)を呼び出し、DB にデータを登録するための構造体を作成します
実装はこんな感じ

domain/model/todo.go
func NewUpdateTodo(id int, task string, status TaskStatus) *Todo {
	return &Todo{
		ID:     id,
		Task:   task,
		Status: status,
	}
}

続いて DB 操作
登録時と同様に t.todoRepository.Update(todo)で repository を介して
infrastructure/todo.goの`Update(t *model.Todo)を呼び出し、DB へのデータの更新を行います

infrastructure/todo.go
func (td *Todo) Update(t *model.Todo) error {
	if err := td.db.Save(t).Error; err != nil {
		return err
	}
	return nil
}

これで更新API の実装が完了したので、実際に API をコールしてデータの更新をしてみたいと思います
先程作成した以下のデータをのステータスを更新します

mysql> select * from todo where id = 1;
+----+------+---------+---------------------+---------------------+
| id | task | status  | created_at          | updated_at          |
+----+------+---------+---------------------+---------------------+
|  1 | test | created | 2023-05-18 02:55:10 | 2023-05-18 02:55:10 |
+----+------+---------+---------------------+---------------------+
1 row in set (0.00 sec)

実際に API をコールして、タスクステータスを created -> processing に更新します。

% curl -i localhost/todo/1 -H "Content-Type: application/json" -X PUT -d '{"task": "test", "status": "processing"}'
HTTP/1.1 204 No Content
Content-Type: application/json; charset=utf-8
Date: Mon, 22 May 2023 16:08:36 GMT

データを確認します

mysql> select * from todo where id = 1;
+----+------+------------+---------------------+---------------------+
| id | task | status     | created_at          | updated_at          |
+----+------+------------+---------------------+---------------------+
|  1 | test | processing | 2023-05-23 01:07:23 | 2023-05-23 01:08:36 |
+----+------+------------+---------------------+---------------------+
1 row in set (0.00 sec)

ちゃんと更新されてますね。よかった
(今となってはステータスだけ更新するAPI にすればよかったな。。。)
これで、更新API は完成です

取得API の実装

取得API では、 パスパラメータに設定した id に該当するデータを1件取得して返します
この部分
/todo/{id}

以下は 取得API の handler の実装です

handler/todo.go
// パス(todo/:id)に指定されたパラメータを格納するための構造体
type FindRequestParam struct {
	ID int `uri:"id" binding:"required"`
}

func (t *todoHandler) Find(c *gin.Context) {
	var req FindRequestParam
    // パスパラメータを構造体(FindRequestParam)にマッピング
    
	if err := c.ShouldBindUri(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
    // usecase の呼び出し
	res, err := t.usecase.Find(req.ID)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}
	if res == nil {
        // 検索結果(res = nil)が存在しなかった場合は 404 not found を返す
		c.JSON(http.StatusNotFound, nil)
		return
	}
    // レスポンスを返却
	c.JSON(http.StatusOK, res)
}

続いて、usecase 層
usecase 層では、 handler から受け取った id 元に検索を行い結果を返します

usecase/todo.go
func (t *todo) Find(id int) (*model.Todo, error) {
	todo, err := t.todoRepository.Find(id)
	if err != nil {
		return nil, err
	}
	return todo, nil
}

infrastructure 層の実装は以下です

infrastructure/todo.go
func (td *Todo) Find(id int) (*model.Todo, error) {
	var todo *model.Todo
	err := td.db.Where("id = ?", id).Take(&todo).Error
	if err != nil {
		if err == gorm.ErrRecordNotFound {
			return nil, nil
		}
		return nil, err
	}
	return todo, nil
}

Take 関数を使った時、レコードが存在しないとgorm.ErrRecordNotFoundというエラーが返ってきます。レコードが存在しない場合にシステムエラーのような扱いをしたくなく、 404 でレスポンスを返したいので、ここでは err !=nilじゃなかった場合、エラーの種類をチェックして エラーがgorm.ErrRecordNotFoundだった場合には nil を返すようにしています

	if err != nil {
		if err == gorm.ErrRecordNotFound {
			return nil, nil
		}
        ...

これで、取得API の実装が完了したので実際に API をコールしてデータが取得できるか確認してみたいと思います
先程利用した以下のデータを取得します

mysql> select * from todo where id = 1;
+----+------+------------+---------------------+---------------------+
| id | task | status     | created_at          | updated_at          |
+----+------+------------+---------------------+---------------------+
|  1 | test | processing | 2023-05-18 02:55:10 | 2023-05-19 01:55:33 |
+----+------+------------+---------------------+---------------------+

API をコールします

% curl -i -XGET localhost/todo/1
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Mon, 22 May 2023 16:10:00 GMT
Content-Length: 114

{"ID":1,"Task":"test","Status":"processing","CreatedAt":"2023-05-23T01:07:23Z","UpdatedAt":"2023-05-23T01:08:36Z"}

ちゃんとデータが取得できますね。よかった

全件取得API の実装

続いて全件取得API の実装です
これは、取得API と違い、 id の指定をせずに、現在 DB に登録されている task を全件取得して返します
※実際に使うときは limit で制限かけるとかした方がいいです

以下は 全件取得API の handler の実装です

handler/todo.go
func (t *todoHandler) FindAll(c *gin.Context) {
	res, err := t.usecase.FindAll()
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}
	c.JSON(http.StatusOK, res)
}

特に目新しいところはないですね
続いて、 usecase です

usecase/todo.go
func (t *todo) FindAll() ([]*model.Todo, error) {
	todo, err := t.todoRepository.FindAll()
	if err != nil {
		return nil, err
	}
	return todo, nil
}

infrastructure

infrastructure/todo.go
func (td *Todo) FindAll() ([]*model.Todo, error) {
	var todos []*model.Todo
	err := td.db.Find(&todos).Error
	if err != nil {
		return nil, err
	}
	return todos, nil
}

gorm の Find 関数では結果が配列で返ってくるので、検索結果を格納するための構造体も配列として定義しています

	var todos []*model.Todo
	err := td.db.Find(&todos).Error
	if err != nil {
		return nil, err
	}

これで、全件取得API の実装が完了したので、実際に API をコールしてデータを取得してみたいと思います
今はデータが1件しか登録されていないので、事前にもう1件登録しておきます
せっかくなので、API で登録しましょう

% curl -i localhost/todo -H "Content-Type: application/json" -X POST -d '{"task": "test2"}'
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Date: Mon, 22 May 2023 16:10:30 GMT
Content-Length: 4

実際にデータが登録されたか確認します

mysql> select * from todo;
+----+-------+------------+---------------------+---------------------+
| id | task  | status     | created_at          | updated_at          |
+----+-------+------------+---------------------+---------------------+
|  1 | test  | processing | 2023-05-23 01:07:23 | 2023-05-23 01:08:36 |
|  2 | test2 | created    | 2023-05-23 01:10:30 | 2023-05-23 01:10:30 |
+----+-------+------------+---------------------+---------------------+
2 rows in set (0.00 sec)

ちゃんと登録されていますね
では、全件取得API でこの2件のデータが期待通り取得できるか試してみます

% curl -i -XGET localhost/todo
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Mon, 22 May 2023 16:11:19 GMT
Content-Length: 229

[{"ID":1,"Task":"test","Status":"processing","CreatedAt":"2023-05-23T01:07:23Z","UpdatedAt":"2023-05-23T01:08:36Z"},{"ID":2,"Task":"test2","Status":"created","CreatedAt":"2023-05-23T01:10:30Z","UpdatedAt":"2023-05-23T01:10:30Z"}]

ちゃんと2件、データが取得できてますね。良かった
見やすいように整形するとこんな感じです

[
	{
		"ID": 1,
		"Task": "test",
		"Status": "processing",
		"CreatedAt": "2023-05-23T01:07:23Z",
		"UpdatedAt": "2023-05-23T01:08:36Z"
	},
	{
		"ID": 2,
		"Task": "test2",
		"Status": "created",
		"CreatedAt": "2023-05-23T01:10:30Z",
		"UpdatedAt": "2023-05-23T01:10:30Z"
	}
]

削除API の実装

最後に、削除API の実装です

削除API は id を指定して対象データの削除を行います
以下は削除API の handler の実装です

handler/todo.go

// パス(todo/:id)に指定されたパラメータを格納するための構造体
type DeleteRequestParam struct {
	ID int `uri:"id"`
}

func (t *todoHandler) Delete(c *gin.Context) {
	var req DeleteRequestParam
    // パスパラメータを構造体(DeleteRequestParam)にマッピング
	if err := c.ShouldBindUri(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
    // usecase の呼び出し
	if err := t.usecase.Delete(req.ID); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}
    // レスポンスを返却
	c.JSON(http.StatusNoContent, nil)
}

続いて usecase

usecase/todo.go
func (t *todo) Delete(id int) error {
	if err := t.todoRepository.Delete(id); err != nil {
		return err
	}
	return nil
}

infrastructure

infrastructure/todo.go
func (td *Todo) Delete(id int) error {
	if err := td.db.Where("id = ?", id).Delete(&model.Todo{}).Error; err != nil {
		return err
	}
	return nil
}

gorm の Delete 関数を使いデータの削除を行っています

これで、削除API の実装が完了したので、実際にデータを削除してみます
以下のデータの削除を行います

mysql> select * from todo where id = 2;
+----+-------+---------+---------------------+---------------------+
| id | task  | status  | created_at          | updated_at          |
+----+-------+---------+---------------------+---------------------+
|  2 | test2 | created | 2023-05-23 01:10:30 | 2023-05-23 01:10:30 |
+----+-------+---------+---------------------+---------------------+
1 row in set (0.00 sec)

削除API の実行

% curl -i localhost/todo/2 -X DELETE
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Mon, 22 May 2023 16:12:41 GMT
Content-Length: 4

いけたっぽい。実際にデータが削除されたか確認します

mysql> select * from todo where id = 2;
Empty set (0.00 sec)

ちゃんと削除されてますね。よかった
以上で、API の実装は完了です

最後に

簡単ですが、基本的な CRUD 操作を行うための REST API を実装しました
今回作ったものは以下のリポジトリに配置しています
一応テストコードも書いているので、興味のある方は見てみてください

次回は githubactions を使って CI の構築でもしてみようと思います
ではでは。

8
7
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
8
7