- Go(versionとかはあんまり気にしなくても大丈夫らしい)
- docker
# create module
go mod init go-rest-api
# start db
docker compose up -d
# 上記または下記
docker-compose up -d
# remove db
docker compose rm -s -f -v
# start app
GO_ENV=dev go run .
# run migrate
GO_ENV=dev go run migrate/migrate.go
※フロント側のコードはここにはないので不要でした
npx create-react-app react-todo --template typescript --use-npm
npm i @tanstack/react-query@4.28.0
npm i @tanstack/react-query-devtools@4.28.0
npm i zustand@4.3.6
npm i @heroicons/react@2.0.16
npm i react-router-dom@6.10.0 axios@1.3.4
https://tailwindcss.com/docs/guides/create-react-app
このWebアプリケーションは model・repository・usecase・conttoller・router という部品から構成されている。それぞれに依存関係があり、図の注射器の絵がそれを示している。
modelはデータの定義、repository はDBと直接のやりとり、usecase はフロントとサーバとの繋ぎ目、controller はフロントから送られてきたデータの操作、router はAPIの部分の処理を示している。
あんまりよくわかってないけど、DB周りの操作だとまず model と repository と db.go・migrate.go あたりから見てみると良さそう。ある程度形ができたら usecase を ripository に対応させるようにコードを書き換えていく必要がある。
データの定義を行う。userというデータを操作したい場合は以下のように定義する。
// ./model/user.go
package model
import "time"
type User struct {
UserID uint `json:"userid" gorm:"primaryKey"`
UserName string `json:"username"`
Email string `json:"email" gorm:"unique"`
Password string `json:"password"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type UserResponse struct {
UserID uint `json:"userid" gorm:"primaryKey"`
UserName string `json:"username"`
Email string `json:"email" gorm:"unique"`
}
また,reviewというデータを操作したい場合は以下のように定義される。
package model
type Review struct {
ReviewID uint `json:"review_id" gorm:"primaryKey"`
ReviewerID string `json:"reviewer_id"`
ReviewerName string `json:"reviewer_name"`
LectureID uint `json:"lecture_id"`
ReviewContent string `json:"review_content"`
ReviewStar uint `json:"review_star"`
}
type ReviewResponse struct {
ReviewID uint `json:"review_id" gorm:"primaryKey"`
LectureID uint `json:"lecture_id"`
ReviewerName string `json:"reviewer_name"`
ReviewContent string `json:"review_content"`
ReviewStar uint `json:"review_star"`
}
以上をもとにしてDBにテーブルが作成されると考えてもよい。UserReseponseはAPIとして返すデータを示している。
GormはGo言語用のフレームワーク。
参考リンク
DBとのやりとりを記述する。定義されたデータ(model)を利用してDBからデータを引っ張ってきたり操作したり。
// ./repository/user_repository.go
package repository
import (
"kadai-notifier/model"
"gorm.io/gorm"
)
// まずインターフェースを定義
type IUserRepository interface {
GetUserByEmail(user *model.User, email string) error
CreateUser(user *model.User) error
}
type userRepository struct {
db *gorm.DB
}
func NewUserRepository(db *gorm.DB) IUserRepository {
return &userRepository{db}
}
// 構造体に対するメソッドを定義している
// ここでは userRepository
func (ur *userRepository) GetUserByEmail(user *model.User, email string) error {
if err := ur.db.Where("email=?", email).First(user).Error; err != nil {
return err
}
return nil
}
func (ur *userRepository) CreateUser(user *model.User) error {
if err := ur.db.Create(user).Error; err != nil {
return err
}
return nil
}
メソッドは以下のように書く
func (対応させたい構造体) メソッド名(引数) 返り値 {
コード
}
repositoryやusecaseなどでは、以下のようにコードを書いていくのがおすすめ。
- stcuctで構造体を定義
- interfaceで構造体に対応するメソッドを定義
- 必要なメソッドを下に書いていく
structを作ると、Pythonでいうインスタンスのようなものを model.User{}
みたいな感じで呼び出せる。
参考サイト
Goで定義された構造体などを受け取ってAPIのレスポンスを返したりするところ。
// ./usercase/user_usecase.go
package usecase
import (
"kadai-notifier/model"
"kadai-notifier/repository"
"kadai-notifier/validator"
"os"
"time"
"github.com/golang-jwt/jwt/v4"
"golang.org/x/crypto/bcrypt"
)
type IUserUsecase interface {
SignUp(user model.User) (model.UserResponse, error)
Login(user model.User) (string, error)
}
type userUsecase struct {
ur repository.IUserRepository
uv validator.IUserValidator
}
func NewUserUsecase(ur repository.IUserRepository, uv validator.IUserValidator) IUserUsecase {
return &userUsecase{ur, uv}
}
func (uu *userUsecase) SignUp(user model.User) (model.UserResponse, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(user.Password), 10)
if err != nil {
return model.UserResponse{}, err
}
newUser := model.User{Email: user.Email, Password: string(hash)}
if err := uu.ur.CreateUser(&newUser); err != nil {
return model.UserResponse{}, err
}
resUser := model.UserResponse{
UserID: newUser.UserID,
Email: newUser.Email,
}
return resUser, nil
}
func (uu *userUsecase) Login(user model.User) (string, error) {
// ユーザが存在するか調べる
if err := uu.uv.UserValidate(user); err != nil {
return "", err
}
// ユーザが存在していれば、保存されているユーザ情報を持ってくる
storedUser := model.User{}
if err := uu.ur.GetUserByEmail(&storedUser, user.Email); err != nil {
return "", err
}
// パスワードが合致するか調べる
err := bcrypt.CompareHashAndPassword([]byte(storedUser.Password), []byte(user.Password))
if err != nil {
return "", err
}
// Cookieにトークンを入れておく
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"user_id": storedUser.UserID,
"exp": time.Now().Add(time.Hour * 12).Unix(),
})
// jwtトークンを生成するための鍵(?)を持ってくる
tokenString, err := token.SignedString([]byte(os.Getenv("SECRET")))
if err != nil {
return "", err
}
return tokenString, nil
}
APIの部分を担ってくれるEchoとのやりとりを示す。Echoからデータを受け取り、usecaseで定義したメソッドを利用しながら実際にAPIを返す部分のrouterに渡すレスポンスを返す。
package controller
import (
"kadai-notifier/model"
"kadai-notifier/usecase"
"net/http"
"os"
"time"
"github.com/labstack/echo/v4"
)
type IUserController interface {
SignUp(c echo.Context) error
LogIn(c echo.Context) error
LogOut(c echo.Context) error
CsrfToken(c echo.Context) error
}
type userController struct {
uu usecase.IUserUsecase
}
func NewUserController(uu usecase.IUserUsecase) IUserController {
return &userController{uu}
}
func (uc *userController) SignUp(c echo.Context) error {
user := model.User{}
if err := c.Bind(&user); err != nil {
return c.JSON(http.StatusBadRequest, err.Error())
}
userRes, err := uc.uu.SignUp(user)
if err != nil {
return c.JSON(http.StatusInternalServerError, err.Error())
}
return c.JSON(http.StatusCreated, userRes)
}
func (uc *userController) LogIn(c echo.Context) error {
user := model.User{}
if err := c.Bind(&user); err != nil {
return c.JSON(http.StatusBadRequest, err.Error())
}
tokenString, err := uc.uu.Login(user)
if err != nil {
return c.JSON(http.StatusInternalServerError, err.Error())
}
cookie := new(http.Cookie)
cookie.Name = "token"
cookie.Value = tokenString
cookie.Expires = time.Now().Add(24 * time.Hour)
cookie.Path = "/"
cookie.Domain = os.Getenv("API_DOMAIN")
cookie.Secure = true
cookie.HttpOnly = true
cookie.SameSite = http.SameSiteNoneMode
c.SetCookie(cookie)
return c.NoContent(http.StatusOK)
}
func (uc *userController) LogOut(c echo.Context) error {
cookie := new(http.Cookie)
cookie.Name = "token"
cookie.Value = ""
cookie.Expires = time.Now()
cookie.Path = "/"
cookie.Domain = os.Getenv("API_DOMAIN")
cookie.Secure = true
cookie.HttpOnly = true
cookie.SameSite = http.SameSiteNoneMode
c.SetCookie(cookie)
return c.NoContent(http.StatusOK)
}
func (uc *userController) CsrfToken(c echo.Context) error {
token := c.Get("csrf").(string)
return c.JSON(http.StatusOK, echo.Map{
"csrf_token": token,
})
}
参考サイト
GETとかPOSTとかを定義するところ。API設計の本とか読んで勉強してみたいな。
package router
import (
"kadai-notifier/controller"
"net/http"
"os"
echojwt "github.com/labstack/echo-jwt/v4"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
func NewRouter(uc controller.IUserController, tc controller.IReviewController) *echo.Echo {
e := echo.New()
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{"http://localhost:3000", os.Getenv("FE_URL")},
AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAccept,
echo.HeaderAccessControlAllowHeaders, echo.HeaderXCSRFToken},
AllowMethods: []string{"GET", "PUT", "POST", "DELETE"},
AllowCredentials: true,
}))
e.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{
CookiePath: "/",
CookieDomain: os.Getenv("API_DOMAIN"),
CookieHTTPOnly: true,
CookieSameSite: http.SameSiteNoneMode,
//CookieSameSite: http.SameSiteDefaultMode,
//CookieMaxAge: 60,
}))
e.POST("/signup", uc.SignUp)
e.POST("/login", uc.LogIn)
e.POST("/logout", uc.LogOut)
e.GET("/csrf", uc.CsrfToken)
t := e.Group("/review")
t.Use(echojwt.WithConfig(echojwt.Config{
SigningKey: []byte(os.Getenv("SECRET")),
TokenLookup: "cookie:token",
}))
t.GET("/:LectureID", tc.GetReview)
t.POST("", tc.PostReview)
return e
}
以上ざっくりとした説明でした。