概要
「Clarity Designを使ったWebシステムを作ってみる」のバックエンド編になります。
バックエンド編ではGoのechoフレームワークを使ったREST APIを作成していきます。
- 準備編
- バックエンド編(本記事)
- フロントエンド編
- デプロイ編
本記事の対象
- go言語でCRUD操作を行いたい方
- echoフレームワークでREST APIを行いたい方
- 本シリーズの続きで読んでくださっている方
本記事での作成物
本記事での作成物は以下gihubに上げております。
いくつか本記事上では割愛をしている部分もあるため、必要な場合はご参照ください。
バックエンド構成
DB構成
詳細なカラムは準備編を参照いただけたらと思いますが、以下3つのテーブルを対象としております。
- surveys
- survey_items
- answers
API構成
かなりざっくりですが、基本的には各テーブルへのCRUDとなります。
パス | メソッド | 引数 | 戻り値 |
---|---|---|---|
/surveys | GET | なし | 全survey |
/surveys/:id | GET | なし(URIパラム) | 指定したidのsurvey |
/surveys | POST | json | 追加されたsurveyのid |
/surveys | PUT | json | なし |
/surveys/:id | DELETE | なし(URIパラム) | なし |
------------ | ------ | ---------- | -------------- |
/surveyitems | GET | なし | 全survey_item |
/surveyitems/:id | GET | なし(URIパラム) | 指定したsurvey_idのsurvey_item |
/surveyitems | POST | json | 追加されたsurvey_itemのid |
/surveyitems | PUT | json | なし |
/surveyitems/:id | DELETE | なし(URIパラム) | なし |
---------------- | ------ | ---------- | ------------------------- |
/answers | GET | なし | 全survey |
/answers/:id | GET | なし(URIパラム) | 指定したsurvey_idのanswer |
/answers | POST | json | 追加されたanswerのid |
/answers | PUT | json | なし |
/answers/:id | DELETE | なし(URIパラム) | なし |
ファイル構成
以下のような構成にしていきます。
app
│ go.mod
│ go.sum
│ main.go
│
├─config
│ config.go
│
├─handler
│ answer.go
│ survey.go
│ survey_item.go
│
├─model
│ answer.go
│ model.go
│ survey.go
│ survey_item.go
│
└─router
router.go
各フォルダの簡易的な説明は以下となります。
フォルダ | 説明 |
---|---|
config | DB接続情報等を記載 |
handler | APIが呼ばれた際にリクエストを捌き、レスポンスを作成 |
model | DBへの処理を実施 |
router | APIのリンクを定義 |
実装
準備
任意のフォルダでgoアプリを作成
go mod init demo-survey
echoフレームワークをインストール
go get github.com/labstack/echo/v4
コーディング
起動用のファイルの作成
一番最初に呼ばれるinitの処理を記載したファイルになります。
echoで1323ポートにて起動を実施させています。
package main
import (
"main/router"
"github.com/labstack/echo"
)
func main() {
e := echo.New()
router.Init(e)
e.Logger.Fatal(e.Start(":1323"))
}
DB接続処理の作成
DB接続に必要なパスやユーザー情報等を記載しておきます。
一旦ローカルのMySQLを今回利用するため以下のように直書きしておりますが、最終的にはサーバー内の環境変数を使うコメントアウト部分を利用予定です。
利用環境に合わせてここは変更が必要になります。
package config
// var (
// DBUser = os.Getenv("DB_USER")
// DBPass = os.Getenv("DB_PASS")
// DBHost = os.Getenv("DB_HOST")
// DBPort = os.Getenv("DB_PORT")
// DBName = os.Getenv("DB_NAME")
// )
var (
DBUser = "root"
DBPass = ""
DBHost = "127.0.0.1"
DBPort = "3306"
DBName = "test"
)
DBとのコネクションを張るための処理を記載します。
前述のconfig.goの情報を使って接続します。
package model
import (
"database/sql"
"main/config"
_ "github.com/go-sql-driver/mysql"
)
var db *sql.DB
func init() {
var err error
db, err = sql.Open("mysql", config.DBUser+":"+config.DBPass+"@tcp("+config.DBHost+":"+config.DBPort+")/"+config.DBName+"?utf8mb4_bin")
if err != nil {
panic(err.Error())
}
}
surveysテーブルへのCRUD処理を実装します。
基本的にはsurvey_itemとanswerも同じ処理なため、本記事上では割愛をさせていただきますが、
github上にまとめてソースは上げておりますため、気になる方はそちらをご参照いただけますと幸いです。
(共通処理をしっかりまとめれば、もっとスマートになると思いつつ。。)
package model
import (
"errors"
"fmt"
_ "github.com/go-sql-driver/mysql"
)
type Survey struct {
ID int `json:"id"`
Title string `json:"title"`
Question string `json:"question"`
Created string `json:"created"`
Updated string `json:"updated"`
}
func GetAllSurveys() ([]*Survey, error) {
rows, err := db.Query("SELECT * FROM surveys")
if err != nil {
return nil, fmt.Errorf("failed to get all survey: %w", err)
}
defer rows.Close()
surveys := []*Survey{}
for rows.Next() {
survey := Survey{}
err = rows.Scan(&survey.ID, &survey.Title, &survey.Question, &survey.Created, &survey.Updated)
if err != nil {
return nil, fmt.Errorf("failed to scan survey: %w", err)
}
surveys = append(surveys, &survey)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("failed to get all surveys: %w", err)
}
return surveys, nil
}
func GetSurveyById(surveyId string) (*Survey, error) {
if surveyId == "" {
return nil, errors.New("empty survey id")
}
survey := Survey{}
err := db.QueryRow("SELECT * FROM surveys WHERE id=?", surveyId).
Scan(&survey.ID, &survey.Title, &survey.Question, &survey.Created, &survey.Updated)
if err != nil {
return nil, fmt.Errorf("failed to get survey: %w", err)
}
return &survey, nil
}
func AddSurvey(survey *Survey) (int, error) {
result, err := db.Exec("INSERT INTO surveys(title, question) VALUES(?,?)", survey.Title, survey.Question)
if err != nil {
return 0, fmt.Errorf("failed to add survey: %w", err)
}
surveyId, err := result.LastInsertId()
if err != nil {
return 0, fmt.Errorf("failed to get insert survey id: %w", err)
}
return int(surveyId), nil
}
func UpdateSurvey(survey *Survey) error {
_, err := db.Exec("UPDATE surveys SET title=?, question=? WHERE id=?", survey.Title, survey.Question, survey.ID)
if err != nil {
return fmt.Errorf("failed to update survey: %w", err)
}
return nil
}
func DeleteSurvey(surveyId string) error {
if surveyId == "" {
return errors.New("empty survey id")
}
result, err := db.Exec("DELETE FROM surveys WHERE id=?", surveyId)
if err != nil {
return fmt.Errorf("failed to delete survey: %w", err)
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to delete survey: %w", err)
}
if rowsAffected == 0 {
return fmt.Errorf("no survey found with id %s", surveyId)
}
return nil
}
handlerの実装
handlerにはAPIへアクセスされた際にリクエストを捌いたり、modelの処理を呼び出したりする処理を記載していきます。
こちらもsurveyのみ記載するため、残りの2ファイルの実装はgithubを参照下さい。
(これもわりと共通処理がががが。。。)
package handler
import (
"net/http"
"main/model"
"github.com/labstack/echo"
)
func GetSurveys(c echo.Context) error {
surveys, err := model.GetAllSurveys()
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusOK, surveys)
}
func GetSurvey(c echo.Context) error {
id := c.Param("id")
survey, err := model.GetSurveyById(id)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusOK, survey)
}
func AddSurvey(c echo.Context) error {
survey := &model.Survey{}
if err := c.Bind(survey); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
}
id, err := model.AddSurvey(survey)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
survey.ID = id
return c.JSON(http.StatusCreated, survey)
}
func UpdateSurvey(c echo.Context) error {
survey := &model.Survey{}
if err := c.Bind(survey); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
}
if err := model.UpdateSurvey(survey); err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusOK, survey)
}
func DeleteSurvey(c echo.Context) error {
id := c.Param("id")
if err := model.DeleteSurvey(id); err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusOK, map[string]string{"success": id})
}
ルーティング処理の作成
APIのルーティング部分を実装していきます。
各アクセスされたURLとメソッドに応じて、対応したhandlerを呼び出しています。
文末周辺にある「AllowMethods」はリクエストの受け取りを許可するために記載しています。
package router
import (
"main/handler"
"net/http"
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
)
func Init(e *echo.Echo) {
e.GET("/surveys", handler.GetSurveys)
e.GET("/surveys/:id", handler.GetSurvey)
e.POST("/surveys", handler.AddSurvey)
e.PUT("/surveys", handler.UpdateSurvey)
e.DELETE("/surveys/:id", handler.DeleteSurvey)
e.GET("/surveyitems", handler.GetAllSurveyItems)
e.GET("/surveyitems/:id", handler.GetSurveyItems)
e.POST("/surveyitems", handler.AddSurveyItem)
e.PUT("/surveyitems", handler.UpdateSurveyItem)
e.DELETE("/surveyitems/:id", handler.DeleteSurveyItem)
e.GET("/answers", handler.GetAnswers)
e.GET("/answers/:id", handler.GetAnswer)
e.POST("/answers", handler.AddAnswer)
e.PUT("/answers", handler.UpdateAnswer)
e.DELETE("/answers/:id", handler.DeleteAnswer)
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{"*"},
AllowMethods: []string{echo.GET, echo.HEAD, echo.PUT, echo.PATCH, echo.POST, echo.DELETE,
http.MethodGet, http.MethodPut, http.MethodPost, http.MethodDelete},
}))
}
動作確認
ここまでで一通りの実装が完了しましたので、起動して動作確認をしていきます。
go run main.go
____ __
/ __/___/ / ___
/ _// __/ _ \/ _ \
/___/\__/_//_/\___/ v3.3.10-dev
High performance, minimalist Go web framework
https://echo.labstack.com
____________________________________O/_______
O\
⇨ http server started on [::]:1323
データの挿入
$ curl -X POST http://127.0.0.1:1323/surveys -H "Content-Type: application/json" -d "{\"title\": \"test\", \"question\": \"do you like tests?\"}"
{"id":1,"title":"test","question":"do you like tests?","created":"","updated":""}
curl -X POST http://127.0.0.1:1323/surveys -H "Content-Type: application/json" -d "{\"title\": \"test\", \"question\": \"What is your favorite season?\"}"
{"id":2,"title":"test","question":"What is your favorite season?","created":"","updated":""}
データの取得
$ curl -X GET http://127.0.0.1:1323/surveys
[{"id":1,"title":"test","question":"do you like tests?","created":"2023-02-16 06:38:33","updated":"2023-02-16 06:38:33"},{"id":2,"title":"test","question":"What is your favorite season?","created":"2023-02-16 06:40:35","updated":"2023-02-16 06:40:35"}]
$ curl -X GET http://127.0.0.1:1323/surveys/2
{"id":2,"title":"test","question":"What is your favorite season?","created":"2023-02-16 06:40:35","updated":"2023-02-16 06:40:35"}
データの更新
$ curl -X PUT http://127.0.0.1:1323/surveys -H "Content-Type: application/json" -d "{\"id\": 2,\"title\": \"season\", \"question\": \"What is your favorite season?\"}"
{"id":2,"title":"season","question":"What is your favorite season?","created":"","updated":""}
データの削除
curl -X DELETE http://127.0.0.1:1323/surveys/2
{"success":"2"}
全部問題なく動いてそうですね。
残りのAPIも同様に動いていることが確認できました。
これにて実装は完了となります。
まとめ
今回はバックエンド編ということでREST APIの実装をいたしました。
普段SpringやFlaskを使ってバックエンドを書くことが多く、goは初めてでしたので、
ちゃんと書けているかわりと不安です。。。
(goは後方互換に対応している分もしかしたら、古い記載もあるかもしれないです。)
ただ、goは思ったより自由に書けて楽しかったので今後もっと使って学んでいければと思います。
次回はようやっと本題のClarity Designを使ったフロントエンド部分です。