こんにちは。KOUKIです。
前回は、JavaScripでTodoリストアプリケーション(UI)を作成しました。
今回は、サーバーサイド側の処理を実装します。
<目次>
前回
サーバーサイド処理
2. Todoの取得
3. Todoのステータス更新
4. Todoの削除
ワークスペース
ワークスペースを準備しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
mkdir ui mv index.html script.js style.css ui mkdir api cd api go mod init todo touch main.go touch Makefile mkdir domain touch domain/todo.go mkdir delivery touch delivery/allget.go touch delivery/statusupdate.go touch delivery/delete.go touch delivery/Store.go mkdir repository touch repository/todo_map.go touch repository/todo_map_test.go mkdir usecase touch usecase/todo_usecase.go $ tree . ├── api │ ├── Makefile │ ├── delivery │ │ ├── allget.go │ │ ├── delete.go │ │ ├── statusupdate.go │ │ └── store.go │ ├── domain │ │ └── todo.go │ ├── go.mod │ ├── main.go │ ├── repository │ │ ├── todo_map.go │ │ └── todo_map_test.go │ └── usecase │ └── todo_usecase.go └── ui ├── index.html ├── script.js └── style.css |
FiberでWebサーバー構築
Fiberを使って、Webサーバーを立てましょう。
インストール
1 2 |
# apiフォルダ配下 go get -u github.com/gofiber/fiber/v2 |
Webサーバーの立ち上げ
公式サイトからWebサーバーのサンプルをコピーして貼り付けます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// main.go package main import "github.com/gofiber/fiber/v2" func main() { app := fiber.New() app.Get("/", func(c *fiber.Ctx) error { return c.SendString("Hello, World 👋!") }) app.Listen(":80") } |
次のコマンドで、Webサーバーを立ち上げてください。
1 2 3 4 5 6 7 8 9 |
$ go run main.go ┌───────────────────────────────────────────────────┐ │ Fiber v2.5.0 │ │ http://127.0.0.1:80 │ │ │ │ Handlers ............. 2 Processes ........... 1 │ │ Prefork ....... Disabled PID .............. 2011 │ └───────────────────────────────────────────────────┘ |
ブラウザから「localhost」にアクセスします。

ホットリロードの導入
Mac(Linux)ユーザー向けの内容になりますが、ホットリロードを導入しましょう。
Go言語はコンパイル型の言語なので、コードの変更を反映させるために、毎回「go run main.go」を実行する必要があります。それだと開発が大変なので、ホットリロードで自動的にビルドするようにします。
Go言語では「reflex」というツールで簡単に導入できますが、Windowsだと動かないかもしれません。
1 2 3 4 5 |
# reflexをインストール go get github.com/cespare/reflex # 実行 reflex -s -r '\.go$$' go run main.go |
実行コマンドが少し複雑なので、Makefileに書いておきましょう。
1 2 3 4 |
.PHONY: run run: reflex -s -r '\.go$$' go run main.go |
1 2 |
# 実行 make run |
これで、ソースコードを保存するとその変更を検知して、ホットリロードが走るようになります。
クリーンアーキテクチャで実装
仕事でAPIを作成するときに、クリーンアーキテクチャを意識して実装しています。
以下の図が有名ですよね。
クリーンアーキテクチャは結構複雑なので、詳細な説明については他のサイトに任せます。
ここで意識したことは、「円の外側から内側にデータが流れるように意識して実装する」ことです。例えば、Use CasesからEntitiesへのアクセスは許可しますが、EntitiesからUse Casesへのアクセスは禁止します。

これにより、密結合が低いアプリケーションを実装することができるようです。
ドメイン
ドメインは、Entities(黄色レイヤ)にあたるところです。ここには、ビジネスロジックを表現するオブジェクトやインターフェースを定義します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// domain/todo.go package domain type Todo struct { ID int `json:"id"` Text string `json:"text"` Completed bool `json:"completed"` } type TodoUsecase interface { AllGet() ([]Todo, error) StatusUpdate(id int) error Store(todo Todo) error Delete(id int) error } type TodoRepository interface { AllGet() ([]Todo, error) StatusUpdate(id int) error Store(todo Todo) error Delete(id int) error } |
Todoは、その名の通りタスクを表現したオブジェクトです。
TodoUsecaseは、ユースケースから呼びだすインターフェースです。
TodoRepositoryは、データベース(sync.Map)にアクセスするためのインターフェースです。
インターフェースをここで実装している理由は、依存性逆転の原則にあるのですが、後ほど説明します。
レポジトリ
レポジトリは、インターフェースアダプター(緑色レイヤ)にあたるところです。ここには、DBへアクセスするためのロジックを記述します。
MySQLやPostgreSQLを使っても良かったのですが、今回は、sync.Mapを使うことにします。
sync.Mapは正確にはDBじゃないのですが、その代用ができます。具体的な使い方は、以下の記事を参考にしてください。
DB操作メソッド
todo_map.goには、DB操作用のメソッドを実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
// repository/todo_map.go package repository import ( "errors" "sync" "todo/domain" ) type todoRepository struct { m sync.Map } // インターフェースを返す func NewSyncMapTodoRepository() domain.TodoRepository { return &todoRepository{} } // 全てのTodoを取得 func (t *todoRepository) AllGet() ([]domain.Todo, error) { var todos []domain.Todo t.m.Range(func(key interface{}, value interface{}) bool { todos = append( todos, // interface型をTodo型に変換 value.(domain.Todo), ) return true }) return todos, nil } // Todoのステータスを更新 func (t *todoRepository) StatusUpdate(id int) error { r, ok := t.m.LoadAndDelete(id) if !ok { return errors.New("Fail Load Data") } newTodo := r.(domain.Todo) if newTodo.Completed { newTodo.Completed = false } else { newTodo.Completed = true } t.Store(newTodo) return nil } // Todoを保存 func (t *todoRepository) Store(todo domain.Todo) error { t.m.Store(todo.ID, todo) return nil } // Todoを削除 func (t *todoRepository) Delete(id int) error { t.m.Delete(id) return nil } |
NewSyncMapTodoRepository関数の戻り値にTodoRepositoryインターフェースを返すところがポイントです。
これは、Domainに実装したインターフェースです。
1 2 3 4 5 6 7 |
// domain/todo.go type TodoRepository interface { AllGet() ([]Todo, error) StatusUpdate(id int) error Store(todo Todo) error Delete(id int) error } |
DB操作メソッドのほとんどでnilを返しているため、何のためにerrorを戻り値にしているのか不思議だと思います。これは、以前紹介した「DB切り替え」の仕組みを導入しているためです。
インターフェースを使ってDBヘのアクセスメソッドを抽象化すると、将来、他のDBに切り替えたくなった時に容易に切り替えできる仕組みを作れます。TodoRepositoryインターフェースを満たせば、利用する側ではDBの種類を意識する必要がなくなるからです。
sync.Mapのメソッドはエラーを返さないものがほとんどですが、他のDB(MySQLやPostgreSQL)ではデータ操作失敗時にエラーを返すので、errorを返すインターフェースを定義しました。
テスト
sync.Mapは戻り値をinterface型で返すなどちょっとした癖があるので、簡単なテストコードを実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
package repository // repository/todo_map_test.go import ( "testing" "todo/domain" "github.com/stretchr/testify/require" ) func TestSyncMapRepository(t *testing.T) { // レポジトリをインスタンス化 // このように操作したいDBのインターフェースを // 指定すればそれに関連するメソッドを呼び出せる dbRepo := NewSyncMapTodoRepository() // 例えば、PostgreSQLがを導入したい場合は、 // 以下のように呼びだすとテストコードの変更を最小限にできる // dbRepo := NewPostgreSQLTodoRepository() id := 1 testData := domain.Todo{ ID: id, Text: "test", Completed: false, } t.Run("Allget Todo Test", func(t *testing.T) { r1, _ := dbRepo.AllGet() require.Empty(t, r1) }) t.Run("Store Todo Test", func(t *testing.T) { dbRepo.Store(testData) r2, _ := dbRepo.AllGet() require.Equal(t, r2[0], testData) }) t.Run("Status Update Test", func(t *testing.T) { dbRepo.StatusUpdate(id) r3, _ := dbRepo.AllGet() require.Equal(t, r3[0].Completed, true) }) t.Run("Delete Todo Test", func(t *testing.T) { dbRepo.Delete(id) r4, _ := dbRepo.AllGet() require.Empty(t, r4) }) } |
テストを実行します。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
$ go test -v ./... === RUN TestSyncMapRepository === RUN TestSyncMapRepository/Allget_Todo_Test === RUN TestSyncMapRepository/Store_Todo_Test === RUN TestSyncMapRepository/Status_Update_Test === RUN TestSyncMapRepository/Delete_Todo_Test --- PASS: TestSyncMapRepository (0.00s) --- PASS: TestSyncMapRepository/Allget_Todo_Test (0.00s) --- PASS: TestSyncMapRepository/Store_Todo_Test (0.00s) --- PASS: TestSyncMapRepository/Status_Update_Test (0.00s) --- PASS: TestSyncMapRepository/Delete_Todo_Test (0.00s) PASS ok todo/repository 0.564s |
OKですね。
コメントにも記載しましたが、「DB切り替え処理」を意識したコードのメリットがここに出てますね。DBを切り替えても呼び出し元のRepositoryを変更すればテストコードの修正も最小限で済みそうです。エラーテストは追加しないといけないですが。
デリバリー
デリバリーは、インターフェースアダプター(緑色レイヤ)にあたるところです。ここでは、ハンドラーを定義し、リクエストを捌きます。
また、各メソッドごとにファイルを分けて実装します。※ファイルを分けておいた方がメンテがしやすい、コンフリクトがしにくいなどのメリットがある
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
// delivery/allget.go package delivery import ( "todo/domain" "github.com/gofiber/fiber/v2" ) // Handlerを定義する type todoAllGetHandler struct { todoUseCase domain.TodoUsecase } func NewTodoAllGetHandler(c *fiber.App, th domain.TodoUsecase) { handler := &todoAllGetHandler{ todoUseCase: th, } c.Get("/todos", handler.AllGet) } func (h *todoAllGetHandler) AllGet(c *fiber.Ctx) error { // UseCaseのAllGetを呼びだす todos, err := h.todoUseCase.AllGet() if err != nil { c.Status(500) return c.JSON(fiber.Map{ "message": "Internal Server Error", }) } return c.JSON(todos) }c |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
// delivery/delete.go package delivery import ( "todo/domain" "github.com/gofiber/fiber/v2" ) // Handlerを定義する type todoDeleteHandler struct { todoUseCase domain.TodoUsecase } func NewTodoDeleteHandler(c *fiber.App, th domain.TodoUsecase) { handler := &todoDeleteHandler{ todoUseCase: th, } c.Post("/todo/delete", handler.Delete) } func (h *todoDeleteHandler) Delete(c *fiber.Ctx) error { todo := new(domain.Todo) err := c.BodyParser(todo) if err != nil { c.Status(400) return c.JSON(fiber.Map{ "message": "Unexpected Request. To check your Todo data", }) } // UseCaseのDeleteを呼びだす err = h.todoUseCase.Delete(todo.ID) if err != nil { c.Status(500) return c.JSON(fiber.Map{ "message": "Internal Server Error", }) } return c.JSON("Delete OK") } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
// delivery/statusupdate.go package delivery import ( "todo/domain" "github.com/gofiber/fiber/v2" ) // Handlerを定義する type todoStatusUpdateHandler struct { todoUseCase domain.TodoUsecase } func NewTodoStatusUpdateHandler(c *fiber.App, th domain.TodoUsecase) { handler := &todoStatusUpdateHandler{ todoUseCase: th, } c.Post("/todo/statusupdate", handler.StatusUpdate) } func (h *todoStatusUpdateHandler) StatusUpdate(c *fiber.Ctx) error { todo := new(domain.Todo) err := c.BodyParser(todo) if err != nil { c.Status(400) return c.JSON(fiber.Map{ "message": "Unexpected Request. To check your ID", }) } // UseCaseのStatusUpdateを呼びだす err = h.todoUseCase.StatusUpdate(todo.ID) if err != nil { c.Status(500) return c.JSON(fiber.Map{ "message": "Internal Server Error", }) } return c.JSON("StatusUpdate OK") } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
// delivery/store.go package delivery import ( "todo/domain" "github.com/gofiber/fiber/v2" ) // Handlerを定義する type todoStoreHandler struct { todoUseCase domain.TodoUsecase } func NewTodoStoreHandler(c *fiber.App, th domain.TodoUsecase) { handler := &todoStoreHandler{ todoUseCase: th, } c.Post("/todo/store", handler.Store) } func (h *todoStoreHandler) Store(c *fiber.Ctx) error { todo := new(domain.Todo) err := c.BodyParser(todo) if err != nil { c.Status(400) return c.JSON(fiber.Map{ "message": "Unexpected Request. To check your Data", }) } // UseCaseのStoreを呼びだす err = h.todoUseCase.Store(*todo) if err != nil { c.Status(500) return c.JSON(fiber.Map{ "message": "Internal Server Error", }) } return c.JSON("Store OK") } |
似たような処理なのでリファクタリングできるかもしれませんが、これでOKです。
ユースケース
ユースケースは、UseCases(赤色レイヤ)にあたるところです。ここには、ビジネスロジックを実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
// usecase/todo_usecase.go package usecase import ( "todo/domain" ) type todoUsecase struct { todoRepo domain.TodoRepository } func NewTodoUsecase(tr domain.TodoRepository) domain.TodoUsecase { return &todoUsecase{ todoRepo: tr, } } func (t *todoUsecase) AllGet() ([]domain.Todo, error) { // 依存性逆転の原則の法則を使って、 // domain経由で上位レイヤーのdeliveryにアクセスしている todos, err := t.todoRepo.AllGet() if err != nil { return nil, err } return todos, nil } func (t *todoUsecase) StatusUpdate(id int) error { err := t.todoRepo.StatusUpdate(id) if err != nil { return err } return nil } func (t *todoUsecase) Store(todo domain.Todo) error { err := t.todoRepo.Store(todo) if err != nil { return err } return nil } func (t *todoUsecase) Delete(id int) error { err := t.todoRepo.Delete(id) if err != nil { return err } return nil } |
個々の役割に紐づく、DB操作メソッドの呼び出しを行いました。
本来であれば、上位レイヤーであるDeliivery(DB操作メソッド)にUseCaseからアクセスしたくありません。それを回避するため、domainにインターフェースを実装し、そのインターフェース経由でdeliveryのメソッドにアクセスしています。
これを依存性逆転の原則と呼ぶそうです。
Interfaceの利便性はこんなところにも発揮されるようですね。
フレームワーク&ドライバー
最後に、フレームワーク&ドライバー(青色レイヤ)を実装します。ここには、Web FrameworkやDB Driverの定義などを実装します。
本アプリでは、Fiberのエンジン設定、レポジトリやユースケースのインスタンス化などがそれに該当します。このレイヤーは最外枠のレイヤーであるため、ここからデータの流れが始まります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
// main.go package main import ( "todo/delivery" "todo/repository" "todo/usecase" "github.com/gofiber/fiber/v2" ) func main() { // repositoryをインスタンス化 tr := repository.NewSyncMapTodoRepository() // usecaseをインスタンス化 tu := usecase.NewTodoUsecase(tr) // fiberをインスタンス化 c := fiber.New() // CORSの設定 c.Use(cors.New(cors.Config{ // https://docs.gofiber.io/api/middleware/cors#config AllowCredentials: true, })) delivery.NewTodoAllGetHandler(c, tu) delivery.NewTodoDeleteHandler(c, tu) delivery.NewTodoStatusUpdateHandler(c, tu) delivery.NewTodoStoreHandler(c, tu) c.Listen(":80") } |
動作確認
Talend API Testerで動作確認をしてみましょう。
今回、4つのエントリポイントを用意しました。
- GET: http://localhost/todos
- POST: http://localhost/todo/delete
- POST: http://localhost/todo/statusupdate
- POST:http://localhost/todo/store
Store
storeは、Todoの追加ができます。 以下のリクエストデータを送信すればOKです。
1 2 3 4 |
{ "id": 1, "text": "First Todo" } |


Todos
次に、todosを試します。これで、格納されたデータを全て取得します。


StatusUpdate
次に、statusupdateを試します。これでcompletedステータスをfalse->trueに変更します。
1 2 3 |
{ "id": 1 } |


todosでデータを取得してみましょう。

completedが、trueになりました。
Delete
最後に、deleteを確認します。
1 2 3 |
{ "id": 1 } |


この状態で、todosを実行するとnullが返却されます。

OKですね。
次回
次回は、 UIと今回実装したバックエンドを繋げてみましょう。
関連記事
こちらの記事も人気です!
コメントを残す
コメントを投稿するにはログインしてください。