Go + TypeScript構成でチュートリアルWebアプリ作ってみる(2) ~バックエンドにCRUDのAPI作成~

## 初めに
### 前回の記事
[Go + TypeScript構成でチュートリアルWebアプリ作ってみる(1) ~サーバ起動まで~](https://qiita.com/minorin_/items/8b3a45ba5276e82935f0)

### 次回の記事
[Go + TypeScript構成でチュートリアルWebアプリ作ってみる(3) ~フロントエンドからAPIを呼び出す~](https://qiita.com/minorin_/items/fb043bf890f6c684f6ad)

### 修正後のディレクトリ構造(フロントエンドは今回は割愛)
クリーンアーキテクチャ構成に変更しました
内側から外側に向かって説明しようと思います
```
backend/
├── cmd/
│   └── main.go // エントリーポイント
│
├── data/
│   └── todos.json // DB構築は面倒なのでJSONにデータ保存
│
├── internal/
│   ├── delivery/
│   │   └── http/
│   │       └── todo_handler.go // HTTPハンドラー
│   │
│   ├── domain/
│   │   └── todo.go // メインモデルとインターフェース
│   │
│   ├── repository/
│   │   └── todo_repository.go // データアクセス
│   │
│   └── usecase/
│       └── todo_usecase.go // ビジネスロジック
│
└── pkg/
    └── server/
        └── server.go // HTTPサーバー設定
```
## ドメイン層
backend/internal/domain/todo.go
ただのinterface定義クラスです
```go
package domain

// model定義
type Todo struct {
	ID    int    `json:"id"`
	Title string `json:"title"`
}

// repositoryのinterface定義
type TodoRepository interface {
	GetAll() ([]Todo, error)
	Create(todo Todo) (Todo, error)
	Update(todo Todo) (Todo, error)
	Delete(id int) error
}

// usecaseのinterface定義
type TodoUsecase interface {
	GetAll() ([]Todo, error)
	Create(todo Todo) (Todo, error)
	Update(todo Todo) (Todo, error)
	Delete(id int) error
}
```

<br>

## リポジトリ層
backend/internal/repository/todo_repository.go
直接のCRUD処理はここに書いています

<details><summary>コード詳細</summary>

```go
package repository

import (
	"encoding/json"
	"errors"
	"os"
	"sync"

	"go-typescript-todo/backend/internal/domain"
)

// TodoRepositoryの構造体定義
type JSONFileTodoRepository struct {
	filePath string
	todos    map[int]domain.Todo
	nextID   int
	mu       sync.RWMutex
}

// インスタンス生成処理
func NewJSONFileTodoRepository(filePath string) (*JSONFileTodoRepository, error) {
	repo := &JSONFileTodoRepository{
		filePath: filePath,
		todos:    make(map[int]domain.Todo),
		nextID:   1,
	}

	// jsonファイルがない場合は作成
	if _, err := os.Stat(filePath); os.IsNotExist(err) {
		if err := repo.saveToFile(); err != nil {
			return nil, err
		}
	} else {
		// jsonファイルが存在している場合は読み込み
		if err := repo.loadFromFile(); err != nil {
			return nil, err
		}
	}

	return repo, nil
}

func (r *JSONFileTodoRepository) loadFromFile() error {
	data, err := os.ReadFile(r.filePath)
	if err != nil {
		return err
	}

	if len(data) == 0 {
		return nil
	}

	var todos []domain.Todo
	if err := json.Unmarshal(data, &todos); err != nil {
		return err
	}

	r.todos = make(map[int]domain.Todo)
	for _, todo := range todos {
		r.todos[todo.ID] = todo
		if todo.ID >= r.nextID {
			r.nextID = todo.ID + 1
		}
	}

	return nil
}

// jsonへの保存処理
func (r *JSONFileTodoRepository) saveToFile() error {
	todos := make([]domain.Todo, 0, len(r.todos))
	for _, todo := range r.todos {
		todos = append(todos, todo)
	}

	data, err := json.MarshalIndent(todos, "", "  ")
	if err != nil {
		return err
	}

	return os.WriteFile(r.filePath, data, 0644)
}

// jsonから取得処理
func (r *JSONFileTodoRepository) GetAll() ([]domain.Todo, error) {
	r.mu.RLock()
	defer r.mu.RUnlock()

	todos := make([]domain.Todo, 0, len(r.todos))
	for _, todo := range r.todos {
		todos = append(todos, todo)
	}
	return todos, nil
}

// 新規レコード作成処理
func (r *JSONFileTodoRepository) Create(todo domain.Todo) (domain.Todo, error) {
	r.mu.Lock()
	defer r.mu.Unlock()

	todo.ID = r.nextID
	r.todos[todo.ID] = todo
	r.nextID++

	if err := r.saveToFile(); err != nil {
		return domain.Todo{}, err
	}

	return todo, nil
}

// レコード更新処理
func (r *JSONFileTodoRepository) Update(todo domain.Todo) (domain.Todo, error) {
	r.mu.Lock()
	defer r.mu.Unlock()

	if _, exists := r.todos[todo.ID]; !exists {
		return domain.Todo{}, errors.New("todo not found")
	}

	r.todos[todo.ID] = todo

	if err := r.saveToFile(); err != nil {
		return domain.Todo{}, err
	}

	return todo, nil
}

// レコード削除処理
func (r *JSONFileTodoRepository) Delete(id int) error {
	r.mu.Lock()
	defer r.mu.Unlock()

	if _, exists := r.todos[id]; !exists {
		return errors.New("todo not found")
	}

	delete(r.todos, id)

	return r.saveToFile()
}
```
</details>

<br>

## ユースケース層
backend/internal/usecase/todo_usecase.go
interface実装しているだけ
```go
package usecase

import (
	"go-typescript-todo/backend/internal/domain"
)

type todoUsecase struct {
	todoRepo domain.TodoRepository
}

// todoUsecaseインスタンスの生成
func NewTodoUsecase(repo domain.TodoRepository) domain.TodoUsecase {
	return &todoUsecase{
		todoRepo: repo,
	}
}

func (u *todoUsecase) GetAll() ([]domain.Todo, error) {
	return u.todoRepo.GetAll()
}

func (u *todoUsecase) Create(todo domain.Todo) (domain.Todo, error) {
	return u.todoRepo.Create(todo)
}

func (u *todoUsecase) Update(todo domain.Todo) (domain.Todo, error) {
	return u.todoRepo.Update(todo)
}

func (u *todoUsecase) Delete(id int) error {
	return u.todoRepo.Delete(id)
}

```

<br>

## HTTPリクエストの処理
backend/internal/delivery/http/todo_handler.go
送信されてきたリクエストの処理をしています。

<details><summary>コード詳細</summary>

```go
package http

import (
	"encoding/json"
	"log"
	"net/http"
	"strconv"

	"go-typescript-todo/backend/internal/domain"
)

// HTTPリクエストを処理する構造体定義
type TodoHandler struct {
	todoUsecase domain.TodoUsecase
}

// インスタンス生成
func NewTodoHandler(usecase domain.TodoUsecase) *TodoHandler {
	return &TodoHandler{
		todoUsecase: usecase,
	}
}

// GetAll処理 /api/todos
func (h *TodoHandler) GetAll(w http.ResponseWriter, r *http.Request) {
	todos, err := h.todoUsecase.GetAll()
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	json.NewEncoder(w).Encode(todos)
}

// Create処理 /api/todos
func (h *TodoHandler) Create(w http.ResponseWriter, r *http.Request) {
	var todo domain.Todo
	if err := json.NewDecoder(r.Body).Decode(&todo); err != nil {
		log.Printf("Error decoding request body: %v", err)
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	log.Printf("Received todo: %+v", todo)

	createdTodo, err := h.todoUsecase.Create(todo)
	if err != nil {
		log.Printf("Error creating todo: %v", err)
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	log.Printf("Created todo: %+v", createdTodo)

	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	w.WriteHeader(http.StatusCreated)
	if err := json.NewEncoder(w).Encode(createdTodo); err != nil {
		log.Printf("Error encoding response: %v", err)
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
}

// Update処理 /api/todos
func (h *TodoHandler) Update(w http.ResponseWriter, r *http.Request) {
	var todo domain.Todo
	if err := json.NewDecoder(r.Body).Decode(&todo); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	updatedTodo, err := h.todoUsecase.Update(todo)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	json.NewEncoder(w).Encode(updatedTodo)
}

// Delete処理 /api/todos?id={id}
func (h *TodoHandler) Delete(w http.ResponseWriter, r *http.Request) {
	idStr := r.URL.Query().Get("id")
	if idStr == "" {
		http.Error(w, "id is required", http.StatusBadRequest)
		return
	}

	id, err := strconv.Atoi(idStr)
	if err != nil {
		http.Error(w, "invalid id", http.StatusBadRequest)
		return
	}

	if err := h.todoUsecase.Delete(id); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	w.WriteHeader(http.StatusNoContent)
}

// 各処理のハンドリング
func (h *TodoHandler) HandleTodos(w http.ResponseWriter, r *http.Request) {
	switch r.Method {
	case http.MethodGet:
		h.GetAll(w, r)
	case http.MethodPost:
		h.Create(w, r)
	case http.MethodPut:
		h.Update(w, r)
	case http.MethodDelete:
		h.Delete(w, r)
	default:
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
	}
}
```
</details>

<br>

## エントリーポイント
backend/cmd/main.go
```go
package main

import (
	"log"
	"net/http"
	"os"
	"path/filepath"

	delivery "go-typescript-todo/backend/internal/delivery/http"
	"go-typescript-todo/backend/internal/repository"
	"go-typescript-todo/backend/internal/usecase"
	"go-typescript-todo/backend/pkg/server"
)

func main() {
	// レコード保存用のディレクトリ作成
	dataDir := filepath.Join("data")
	if err := os.MkdirAll(dataDir, 0755); err != nil {
		log.Fatalf("Failed to create data directory: %v", err)
	}

	// repositoryのインスタンス生成
	todoRepo, err := repository.NewJSONFileTodoRepository(filepath.Join(dataDir, "todos.json"))
	if err != nil {
		log.Fatalf("Failed to initialize repository: %v", err)
	}

	// usecaseにDI
	todoUsecase := usecase.NewTodoUsecase(todoRepo)

	// HTTPにDI
	todoHandler := delivery.NewTodoHandler(todoUsecase)

	// マルチプレクサ生成
	mux := http.NewServeMux()
	mux.HandleFunc("/api/todos", todoHandler.HandleTodos)

	// サーバー起動
	srv := server.NewServer()
	srv.SetupRoutes(mux)

	log.Printf("Server starting on :8080")
	if err := srv.Start(":8080"); err != nil {
		log.Fatalf("Server failed to start: %v", err)
	}
}
```
<br>

## サーバー設定
backend/pkg/server/server.go

```go
package server

import (
	"log"
	"net/http"

	"github.com/gin-gonic/gin"
)

// Server構造体定義
type Server struct {
	router *gin.Engine
}

// Serverインスタンス生成
func NewServer() *Server {
	router := gin.Default()
	return &Server{
		router: router,
	}
}

// セットアップ
func (s *Server) SetupRoutes(mux *http.ServeMux) {
	s.router.Any("/*path", func(c *gin.Context) {
		handler := mux
		handler.ServeHTTP(c.Writer, c.Request)
	})
}

// サーバー起動
func (s *Server) Start(addr string) error {
	log.Printf("Server starting on %s", addr)
	return s.router.Run(addr)
}
```

<br>

## API確認
jsonファイルにレコードが存在しないことを確認する
```
$ cat data/todos.json
[]
```

<br>

画面上でレコードが存在しないことを確認する
http://localhost:8080/api/todosにアクセス

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3942091/bcb35061-88ed-4871-93d2-609adef7bf19.png)

<br>

curlで新しいレコードを作成してみる
```
$body = @{
    title = "散歩に出かける"
} | ConvertTo-Json -Compress

$bodyBytes = [System.Text.Encoding]::UTF8.GetBytes($body)

Invoke-RestMethod -Method Post -Uri "http://localhost:8080/api/todos" -ContentType "application/json; charset=utf-8" -Body $bodyBytes
```

<br>

jsonファイルに追加されていることを確認する
```
cat data/todos.json
[
  {
    "id": 1,
    "title": "散歩に出かける"
  }
]
```

<br>

画面上でも確認してみる
http://localhost:8080/api/todosにアクセス

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3942091/691ae9f8-46af-47f9-bfe5-732a5d2f0e89.png)


OK！！


## 各層まとめ
### 1.Domain Layer (internal/domain/todo.go)
アプリケーションの中心となるモデルとインターフェース
ビジネスロジックの定義
他のレイヤーへの依存がない

### 2.Repository Layer (internal/repository/todo_repository.go)
データの永続化を担当
JSONファイルを使用した実装
Domain Layerのインターフェースを実装

### 3.Usecase Layer (internal/usecase/todo_usecase.go)
ビジネスロジックの実装
Repository Layerを使用
Domain Layerのインターフェースを実装

### 4.Delivery Layer (internal/delivery/http/todo_handler.go)
HTTPリクエストの処理
Usecase Layerを使用
クライアントとの通信を担当

### 5.Server (pkg/server/server.go)
HTTPサーバーの設定
ルーティングの設定

### 6.Main (cmd/main.go)
アプリケーションのエントリーポイント
各レイヤーの初期化と接続