goxygenをつかって、Go + Front Framework + MongoDB構成のWebアプリテンプレートをかんたんに作成する

## 前置き
githubの[Trending Go repositories](https://github.com/trending/go)を見ていたところ、**goxygen**という面白そうなテンプレート作成ツールがあったので、使ってみました。

リポジトリは下記です。
https://github.com/Shpota/goxygen

リポジトリのトップページに、

*Generate a modern Web project with Go, Angular/React/Vue, and MongoDB in seconds🚀*

とあるように、コマンド一発叩くだけで、MariaDB + golang + FrontFW(Angular or React or Vue)構成のWebアプリの骨子が出来上がるみたいです。

## インストール
READMEに記載の通り、`go get -u github.com/shpota/goxygen`にて取得します。

## goxygen起動
`go run github.com/shpota/goxygen init [appname]`一発でOKです。
なお、デフォルトはReactなので、ほかのFrontFWを使いたい場合は、`--frontend`オプションを使用します。
vueの場合は、`go run github.com/shpota/goxygen init --frontend vue [appname]`です。

実行すると、下記のような構成のディレクトリがgenerateされます。(vueで起動した場合)

```
.
├── Dockerfile
├── README.md
├── docker-compose-dev.yml
├── docker-compose.yml
├── init-db.js
├── server
│   ├── db
│   │   └── db.go
│   ├── go.mod
│   ├── model
│   │   └── technology.go
│   ├── server.go
│   └── web
│       └── app.go
└── webapp
    ├── babel.config.js
    ├── package.json
    ├── public
    │   ├── favicon.ico
    │   └── index.html
    └── src
        ├── App.vue
        ├── assets
        ├── components
        └── main.js

9 directories, 16 files
```

webappフォルダがFrontのソース、serverフォルダがserverのソースとなっています。


## 動かしてみる(Docker)
READMEに記載の通り、`docker-compose up`コマンドをたたけばOKです。
dockerが正常に立ち上がったら、http://localhost:8080 で下記のようなサンプルページにアクセスできると思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153208/a88fe5e0-efb8-8506-d162-adba56b1699a.png)

## 動かしてみる(ローカル)
ローカルでの動作させる場合は、docker-compose-dev.ymlを使えば良さそうです。
DBのみのコンテナが準備されています。
開発で頻繁にコードを触る場合は、こちらのdocker-composeファイルのほうがお世話になりそうですね。

* `docker-compose -f docker-compose-dev.yml up`でDB起動
* serverディレクトリ内で`go run server.go`でサーバ起動
* webappディレクトリ内で`npm install` `npm run start`でクライアント起動

上記コマンド後、http://localhost:3000 でサンプルページにアクセスすることができます。
## Todoアプリを作ってみる
generate時にあったサンプルコードをもとに、Todoアプリを作ってみました。
### webapp側の実装
`App.vue`にまとめて記載していますが、本来はコンポーネントに分けて実装されることになります。

```vuejs:src/App.vue
<template>
  <div>
    <h3>My Todo</h3>
    <input v-model="newTodo" placeholder="input here..."> 
    <button @click="addTodo()">ADD</button>

    <h5>Todo List</h5>
    <ul>
      <li v-for="todo in todos" :key="todo.id">
        <div :id="todo.id">
        {{ todo.text }}
        <button @click="deleteTodo(todo.id)">DEL</button>
      </div>
    </li>
    </ul>
  </div>
</template>

<script>
import axios from 'axios'
export default {
  data() {
    return {
      todos: [],
      newTodo: ''
    }
  },
  mounted() {
    this.fetchTodo()
  },
  methods: {
    fetchTodo() {
      axios
      .get(`${process.env.VUE_APP_API_URL}/api/todos`)
      .then(response => (this.todos = response.data))
    },
    addTodo() {
      if (this.newTodo==='') return;
      axios
        .post(`${process.env.VUE_APP_API_URL}/api/todos`, {
          text: this.newTodo
        })
        .then(() => this.fetchTodo())
      this.newTodo = ''
    },
    deleteTodo(i) {
      axios
        .delete(`${process.env.VUE_APP_API_URL}/api/todos?id=${i}`)
        .then(() => this.fetchTodo())
    }
  }
}
</script>
```
サーバ側とのやり取りは、axiosによるhttp通信で実現しています。

### server側の実装
つづいてサーバ側の実装です。
generate時のサンプルコードを元にしているので、サンプルコードに追記する形で実装しました。
追記箇所には、`//追加`のコメントアウトを入れています。

```golang:web/app.go
package web

import (
	"bytes"
	"encoding/json"
	"io"
	"log"
	"my-app2/db"
	"net/http"
)

type App struct {
	d        db.DB
	handlers map[string]http.HandlerFunc
}

// 追加
type TodoForm struct {
	Text string `json:"text"`
}

func NewApp(d db.DB, cors bool) App {
	app := App{
		d:        d,
		handlers: make(map[string]http.HandlerFunc),
	}
	techHandler := app.GetTechnologies
	// 追加
	todoHandler := app.Todos
	if !cors {
		techHandler = disableCors(techHandler)
		// 追加
		todoHandler = disableCors(todoHandler)
	}
	app.handlers["/api/technologies"] = techHandler
	// 追加
	app.handlers["/api/todos"] = todoHandler
	app.handlers["/"] = http.FileServer(http.Dir("/webapp")).ServeHTTP
	return app
}

func (a *App) Serve() error {
	for path, handler := range a.handlers {
		http.Handle(path, handler)
	}
	log.Println("Web server is available on port 8080")
	return http.ListenAndServe(":8080", nil)
}

func (a *App) GetTechnologies(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	technologies, err := a.d.GetTechnologies()
	if err != nil {
		sendErr(w, http.StatusInternalServerError, err.Error())
		return
	}
	err = json.NewEncoder(w).Encode(technologies)
	if err != nil {
		sendErr(w, http.StatusInternalServerError, err.Error())
	}
}

// 追加
func (a *App) Todos(w http.ResponseWriter, r *http.Request) {
	switch r.Method {
	case http.MethodGet:
		w.Header().Set("Content-Type", "application/json")
		todos, err := a.d.GetTodos()
		if err != nil {
			sendErr(w, http.StatusInternalServerError, err.Error())
			return
		}
		err = json.NewEncoder(w).Encode(todos)
		if err != nil {
			sendErr(w, http.StatusInternalServerError, err.Error())
		}
	case http.MethodPost:
		body := r.Body
		defer body.Close()
		buf := new(bytes.Buffer)
		io.Copy(buf, body)

		var todo TodoForm
		json.Unmarshal(buf.Bytes(), &todo)
		err := a.d.PostTodos(todo.Text)
		if err != nil {
			sendErr(w, http.StatusInternalServerError, err.Error())
			return
		}

	case http.MethodDelete:
		err := a.d.DeleteTodos(r.FormValue("id"))
		if err != nil {
			sendErr(w, http.StatusInternalServerError, err.Error())
			return
		}
	}
}

func sendErr(w http.ResponseWriter, code int, message string) {
	resp, _ := json.Marshal(map[string]string{"error": message})
	http.Error(w, string(resp), code)
}

// Needed in order to disable CORS for local developazsment
func disableCors(h http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Access-Control-Allow-Origin", "*")
		w.Header().Set("Access-Control-Allow-Methods", "*")
		w.Header().Set("Access-Control-Allow-Headers", "*")
		h(w, r)
	}
}

```

```golang:db/db.go
package db

import (
	"context"
	"log"
	"my-app2/model"

	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/bson/primitive"
	"go.mongodb.org/mongo-driver/mongo"
)

type DB interface {
	GetTechnologies() ([]*model.Technology, error)
	// 追加
	GetTodos() ([]*model.Todo, error)
	PostTodos(string) error
	DeleteTodos(string) error
}

type MongoDB struct {
	collection *mongo.Collection
	// 追加
	todoCl *mongo.Collection
}

func NewMongo(client *mongo.Client) DB {
	tech := client.Database("tech").Collection("tech")
	// 追加
	todo := client.Database("todo").Collection("todo")
	return MongoDB{collection: tech, todoCl: todo}
}

func (m MongoDB) GetTechnologies() ([]*model.Technology, error) {
	res, err := m.collection.Find(context.TODO(), bson.M{})
	if err != nil {
		log.Println("Error while fetching technologies:", err.Error())
		return nil, err
	}
	var tech []*model.Technology
	err = res.All(context.TODO(), &tech)
	if err != nil {
		log.Println("Error while decoding technologies:", err.Error())
		return nil, err
	}
	return tech, nil
}

// 追加
func (m MongoDB) GetTodos() ([]*model.Todo, error) {
	res, err := m.todoCl.Find(context.TODO(), bson.M{})
	if err != nil {
		log.Println("Error while fetching todos:", err.Error())
		return nil, err
	}
	var todo []*model.Todo
	err = res.All(context.TODO(), &todo)
	if err != nil {
		log.Println("Error while decoding todos:", err.Error())
		return nil, err
	}
	return todo, nil
}

// 追加
func (m MongoDB) PostTodos(text string) error {
	mdl := model.Todo{ID: primitive.NewObjectID(), Text: text}
	_, err := m.todoCl.InsertOne(context.TODO(), mdl)
	if err != nil {
		log.Println("Error while posting todos:", err.Error())
		return err
	}
	return nil
}

// 追加
func (m MongoDB) DeleteTodos(id string) error {
	objectID, _ := primitive.ObjectIDFromHex(id)
	_, err := m.todoCl.DeleteOne(context.TODO(), bson.M{"_id": objectID})
	if err != nil {
		log.Println("Error while deleting todos:", err.Error())
		return err
	}
	return nil
}
```
また、modelパッケージにtodo.goを追加しておきます。

```golang:model/todo.go
package model

import "go.mongodb.org/mongo-driver/bson/primitive"

// 追加
type Todo struct {
	ID   primitive.ObjectID `json:"id" bson:"_id"`
	Text string             `json:"text" bson:"text"`
}
```

この状態で、非常にシンプルではありますが、
DBへの参照・登録・削除機能を持ったTODOアプリを起動することができます。

![goxygen.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153208/a86735da-d60a-cfdd-39ac-98f9f8d54299.gif)

※ソースの全量は[こちら](https://github.com/knry0329/goxygen-todo
)に。

## まとめ
コマンド一発でDB含めた環境が準備でき、かつサンプルコードがついてくるので、それをもとにしてwebアプリを作っていくことができそうです。
golangをベースに新規Webアプリを作ろうとしている人には、とても便利なツールだと思います。
現在、起動時の引数でAngular/React/Vueを選択することができますが、その他、DBが選べたりできるようになると、もっと汎用的に使えそうですね。