初めに
前回の記事
Go + TypeScript構成でチュートリアルWebアプリ作ってみる(1) ~サーバ起動まで~
次回の記事
Go + TypeScript構成でチュートリアルWebアプリ作ってみる(3) ~フロントエンドからAPIを呼び出す~
修正後のディレクトリ構造(フロントエンドは今回は割愛)
クリーンアーキテクチャ構成に変更しました
内側から外側に向かって説明しようと思います
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定義クラスです
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
}
リポジトリ層
backend/internal/repository/todo_repository.go
直接のCRUD処理はここに書いています
コード詳細
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()
}
ユースケース層
backend/internal/usecase/todo_usecase.go
interface実装しているだけ
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)
}
HTTPリクエストの処理
backend/internal/delivery/http/todo_handler.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)
}
}
エントリーポイント
backend/cmd/main.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)
}
}
サーバー設定
backend/pkg/server/server.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)
}
API確認
jsonファイルにレコードが存在しないことを確認する
$ cat data/todos.json
[]
画面上でレコードが存在しないことを確認する
http://localhost:8080/api/todosにアクセス
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
jsonファイルに追加されていることを確認する
cat data/todos.json
[
{
"id": 1,
"title": "散歩に出かける"
}
]
画面上でも確認してみる
http://localhost:8080/api/todosにアクセス
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)
アプリケーションのエントリーポイント
各レイヤーの初期化と接続

