├── go.mod ├── domain ├── service │ ├── user_name_validator.go │ └── user_registration_checker.go ├── model │ ├── error.go │ ├── post.go │ └── user.go └── repository │ ├── post.go │ └── user.go ├── main.go ├── persistence └── inmemory │ ├── export.go │ ├── database.go │ └── dto │ ├── user.go │ └── post.go ├── usecase ├── error.go ├── get_user.go ├── get_user_post_list.go ├── create_user.go └── create_user_post.go ├── go.sum ├── utils └── hash.go ├── controller ├── view │ ├── user.go │ └── post.go ├── router │ └── serve.go └── handler │ ├── get_user.go │ ├── get_user_post_list.go │ ├── create_user.go │ └── create_user_post.go ├── impl ├── service │ ├── user_name_validator.go │ └── user_registration_checker.go └── repository │ ├── post.go │ ├── user.go │ └── user_test.go └── README.md /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mahiro72/go-api-sample 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/go-chi/chi/v5 v5.0.10 7 | github.com/google/uuid v1.3.1 8 | ) 9 | -------------------------------------------------------------------------------- /domain/service/user_name_validator.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import "context" 4 | 5 | type UserNameValidator interface { 6 | CheckAlreadyUsed(ctx context.Context, name string) (bool, error) 7 | } 8 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/mahiro72/go-api-sample/controller/router" 7 | ) 8 | 9 | func main() { 10 | fmt.Println("Server Started") 11 | router.Serve() 12 | } 13 | -------------------------------------------------------------------------------- /persistence/inmemory/export.go: -------------------------------------------------------------------------------- 1 | package inmemory 2 | 3 | import ( 4 | "github.com/mahiro72/go-api-sample/persistence/inmemory/dto" 5 | ) 6 | 7 | // test用のDatabaseリセット関数 8 | func ResetDatabase() error { 9 | database = Database{ 10 | UserList: []dto.User{}, 11 | PostList: []dto.Post{}, 12 | } 13 | return nil 14 | } 15 | -------------------------------------------------------------------------------- /usecase/error.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import "errors" 4 | 5 | var ErrFieldsValidation = errors.New("failure fields validation") 6 | var ErrIDValidation = errors.New("failure id validation") 7 | var ErrNotExistsData = errors.New("not exists data") 8 | 9 | // user 10 | var ErrUserNameAlreadyUsed = errors.New("user name already used") 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= 2 | github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= 3 | github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= 4 | github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 5 | -------------------------------------------------------------------------------- /utils/hash.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "crypto/sha256" 5 | "encoding/hex" 6 | ) 7 | 8 | func getSHA256Binary(s string) []byte { 9 | r := sha256.Sum256([]byte(s)) 10 | return r[:] 11 | } 12 | 13 | func CreateHashFromString(s string) string { 14 | b := getSHA256Binary(s) 15 | h := hex.EncodeToString(b) 16 | return h 17 | } 18 | -------------------------------------------------------------------------------- /domain/service/user_registration_checker.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/mahiro72/go-api-sample/domain/model" 8 | ) 9 | 10 | var ErrNoUserFound = errors.New("no user found") 11 | 12 | type UserRegistrationChecker interface { 13 | CheckUserRegistration(ctx context.Context, name, password string) (model.UserID, error) 14 | } 15 | -------------------------------------------------------------------------------- /domain/model/error.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "errors" 4 | 5 | // user 6 | var ErrUserFieldsValidation = errors.New("failure user fields validation") 7 | var ErrUserIDValidation = errors.New("failure userID validation") 8 | 9 | // post 10 | var ErrPostFieldsValidation = errors.New("failure post fields validation") 11 | var ErrPostIDValidation = errors.New("failure postID validation") 12 | -------------------------------------------------------------------------------- /persistence/inmemory/database.go: -------------------------------------------------------------------------------- 1 | package inmemory 2 | 3 | import ( 4 | "github.com/mahiro72/go-api-sample/persistence/inmemory/dto" 5 | ) 6 | 7 | // FIXME: lockが必要 8 | type Database struct { 9 | UserList []dto.User 10 | PostList []dto.Post 11 | } 12 | 13 | var database Database 14 | 15 | func init() { 16 | database = Database{ 17 | UserList: []dto.User{}, 18 | PostList: []dto.Post{}, 19 | } 20 | } 21 | 22 | func NewDatabase() *Database { 23 | return &database 24 | } 25 | -------------------------------------------------------------------------------- /domain/repository/post.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/mahiro72/go-api-sample/domain/model" 8 | ) 9 | 10 | var ErrNoPostFound = errors.New("no post found") 11 | 12 | type Post interface { 13 | Create(ctx context.Context, post model.Post, userID model.UserID) error 14 | Find(ctx context.Context, postID model.PostID) (model.Post, error) 15 | FindAllByUserID(ctx context.Context, userID model.UserID) ([]model.Post, error) 16 | } 17 | -------------------------------------------------------------------------------- /controller/view/user.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/mahiro72/go-api-sample/domain/model" 7 | ) 8 | 9 | type user struct { 10 | ID string `json:"id"` 11 | Name string `json:"name"` 12 | Password string `json:"-"` 13 | CreatedAt time.Time `json:"created_at"` 14 | } 15 | 16 | func NewUser(m model.User) user { 17 | return user{ 18 | ID: m.ID.String(), 19 | Name: m.Name, 20 | Password: m.Password, 21 | CreatedAt: m.CreatedAt, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /domain/repository/user.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/mahiro72/go-api-sample/domain/model" 8 | ) 9 | 10 | var ErrNoUserFound = errors.New("no user found") 11 | 12 | type User interface { 13 | Create(ctx context.Context, user model.User) error 14 | Find(ctx context.Context, userID model.UserID) (model.User, error) 15 | FindByName(ctx context.Context, name string) (model.User, error) 16 | FindByNameAndPassword(ctx context.Context, name, password string) (model.User, error) 17 | } 18 | -------------------------------------------------------------------------------- /impl/service/user_name_validator.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/mahiro72/go-api-sample/domain/repository" 8 | ) 9 | 10 | type userNameValidator struct { 11 | repoUser repository.User 12 | } 13 | 14 | func NewUserNameValidator(repoUser repository.User) *userNameValidator { 15 | return &userNameValidator{ 16 | repoUser: repoUser, 17 | } 18 | } 19 | 20 | func (svc *userNameValidator) CheckAlreadyUsed(ctx context.Context, name string) (bool, error) { 21 | _, err := svc.repoUser.FindByName(ctx, name) 22 | if errors.Is(err, repository.ErrNoUserFound) { 23 | return false, nil 24 | } 25 | return true, err 26 | } 27 | -------------------------------------------------------------------------------- /persistence/inmemory/dto/user.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/mahiro72/go-api-sample/domain/model" 7 | ) 8 | 9 | type User struct { 10 | ID string 11 | Name string 12 | Password string 13 | CreatedAt time.Time 14 | } 15 | 16 | func NewUser(m model.User) User { 17 | return User{ 18 | ID: m.ID.String(), 19 | Name: m.Name, 20 | Password: m.Password, 21 | CreatedAt: m.CreatedAt, 22 | } 23 | } 24 | 25 | func (dto User) ToModel() model.User { 26 | return model.User{ 27 | ID: model.UserID(dto.ID), 28 | Name: dto.Name, 29 | Password: dto.Password, 30 | CreatedAt: dto.CreatedAt, 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /controller/view/post.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/mahiro72/go-api-sample/domain/model" 7 | ) 8 | 9 | type post struct { 10 | ID string `json:"id"` 11 | Title string `json:"title"` 12 | Content string `json:"content"` 13 | CreatedAt time.Time `json:"created_at"` 14 | } 15 | 16 | func NewPost(m model.Post) post { 17 | return post{ 18 | ID: m.ID.String(), 19 | Title: m.Title, 20 | Content: m.Content, 21 | CreatedAt: m.CreatedAt, 22 | } 23 | } 24 | 25 | func NewPostList(ms []model.Post) []post { 26 | r := []post{} 27 | 28 | for _, m := range ms { 29 | r = append(r, NewPost(m)) 30 | } 31 | return r 32 | } 33 | -------------------------------------------------------------------------------- /persistence/inmemory/dto/post.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/mahiro72/go-api-sample/domain/model" 7 | ) 8 | 9 | type Post struct { 10 | ID string 11 | UserID string 12 | Title string 13 | Content string 14 | CreatedAt time.Time 15 | } 16 | 17 | func NewPost(m model.Post, userID model.UserID) Post { 18 | return Post{ 19 | ID: m.ID.String(), 20 | UserID: userID.String(), 21 | Title: m.Title, 22 | Content: m.Content, 23 | CreatedAt: m.CreatedAt, 24 | } 25 | } 26 | 27 | func (dto Post) ToModel() model.Post { 28 | return model.Post{ 29 | ID: model.PostID(dto.ID), 30 | Title: dto.Title, 31 | Content: dto.Content, 32 | CreatedAt: dto.CreatedAt, 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /controller/router/serve.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | 7 | "github.com/mahiro72/go-api-sample/controller/handler" 8 | 9 | "github.com/go-chi/chi/v5" 10 | ) 11 | 12 | func Serve() { 13 | r := chi.NewRouter() 14 | 15 | r.Get("/health", func(w http.ResponseWriter, r *http.Request) { 16 | w.WriteHeader(http.StatusOK) 17 | w.Write([]byte("health ok!")) 18 | }) 19 | 20 | r.Route("/users", func(r chi.Router) { 21 | r.Post("/", handler.CreateUser) 22 | r.Get("/{userID}", handler.GetUser) 23 | }) 24 | 25 | r.Route("/users/{userID}/posts", func(r chi.Router) { 26 | r.Post("/", handler.CreateUserPost) 27 | r.Get("/list", handler.GetUserPostList) 28 | }) 29 | if err := http.ListenAndServe(":8080", r); err != nil { 30 | log.Fatal(err) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /domain/model/post.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/google/uuid" 7 | ) 8 | 9 | type PostID string 10 | 11 | func NewPostID() PostID { 12 | return PostID(uuid.NewString()) 13 | } 14 | 15 | func (p PostID) String() string { 16 | return string(p) 17 | } 18 | 19 | func (p PostID) IsSame(other PostID) bool { 20 | return p.String() == other.String() 21 | } 22 | 23 | type Post struct { 24 | //投稿を一意に識別するID 25 | ID PostID 26 | //投稿のタイトル 27 | Title string 28 | //投稿にの内容 29 | Content string 30 | //投稿の作成日 31 | CreatedAt time.Time 32 | } 33 | 34 | func NewPost(title, content string) (Post, error) { 35 | if len(title) > 200 { 36 | return Post{}, ErrUserFieldsValidation 37 | } 38 | if len(content) > 600 { 39 | return Post{}, ErrUserFieldsValidation 40 | } 41 | 42 | return Post{ 43 | ID: NewPostID(), 44 | Title: title, 45 | Content: content, 46 | CreatedAt: time.Now(), 47 | }, nil 48 | } 49 | -------------------------------------------------------------------------------- /usecase/get_user.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/mahiro72/go-api-sample/domain/model" 8 | "github.com/mahiro72/go-api-sample/domain/repository" 9 | ) 10 | 11 | type getUser struct { 12 | repoUser repository.User 13 | } 14 | 15 | func NewGetUser(repoUser repository.User) *getUser { 16 | return &getUser{ 17 | repoUser: repoUser, 18 | } 19 | } 20 | 21 | func (uc *getUser) Exec(ctx context.Context, userIDString string) (model.User, error) { 22 | userID, err := model.ParseUserID(userIDString) 23 | if err != nil { 24 | if errors.Is(err, model.ErrUserIDValidation) { 25 | return model.User{}, ErrIDValidation 26 | } 27 | return model.User{}, err 28 | } 29 | 30 | user, err := uc.repoUser.Find(ctx, userID) 31 | if err != nil { 32 | if errors.Is(err, repository.ErrNoUserFound) { 33 | return model.User{}, ErrNotExistsData 34 | } 35 | return model.User{}, err 36 | } 37 | return user, nil 38 | } 39 | -------------------------------------------------------------------------------- /impl/service/user_registration_checker.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/mahiro72/go-api-sample/domain/model" 8 | "github.com/mahiro72/go-api-sample/domain/repository" 9 | "github.com/mahiro72/go-api-sample/domain/service" 10 | "github.com/mahiro72/go-api-sample/utils" 11 | ) 12 | 13 | type userRegistrationChecker struct { 14 | repoUser repository.User 15 | } 16 | 17 | func NewUserRegistrationChecker(repoUser repository.User) *userRegistrationChecker { 18 | return &userRegistrationChecker{ 19 | repoUser: repoUser, 20 | } 21 | } 22 | 23 | func (svc *userRegistrationChecker) CheckUserRegistration(ctx context.Context, name, password string) (model.UserID, error) { 24 | hashPwd := utils.CreateHashFromString(name + password) 25 | 26 | user, err := svc.repoUser.FindByNameAndPassword(ctx, name, hashPwd) 27 | if err != nil { 28 | if errors.Is(err, repository.ErrNoUserFound) { 29 | return "", service.ErrNoUserFound 30 | } 31 | return "", err 32 | } 33 | return user.ID, nil 34 | } 35 | -------------------------------------------------------------------------------- /controller/handler/get_user.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "net/http" 7 | 8 | "github.com/mahiro72/go-api-sample/controller/view" 9 | "github.com/mahiro72/go-api-sample/impl/repository" 10 | "github.com/mahiro72/go-api-sample/persistence/inmemory" 11 | "github.com/mahiro72/go-api-sample/usecase" 12 | 13 | "github.com/go-chi/chi/v5" 14 | ) 15 | 16 | func GetUser(w http.ResponseWriter, r *http.Request) { 17 | userIDString := chi.URLParam(r, "userID") 18 | 19 | db := inmemory.NewDatabase() 20 | uc := usecase.NewGetUser(repository.NewUser(db)) 21 | user, err := uc.Exec(r.Context(), userIDString) 22 | if err != nil { 23 | switch { 24 | case errors.Is(err, usecase.ErrIDValidation): 25 | w.WriteHeader(http.StatusBadRequest) 26 | return 27 | case errors.Is(err, usecase.ErrNotExistsData): 28 | w.WriteHeader(http.StatusNotFound) 29 | return 30 | default: 31 | w.WriteHeader(http.StatusInternalServerError) 32 | return 33 | } 34 | } 35 | 36 | res := view.NewUser(user) 37 | b, err := json.Marshal(res) 38 | if err != nil { 39 | w.WriteHeader(http.StatusInternalServerError) 40 | return 41 | } 42 | 43 | w.WriteHeader(http.StatusOK) 44 | w.Write(b) 45 | } 46 | -------------------------------------------------------------------------------- /usecase/get_user_post_list.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/mahiro72/go-api-sample/domain/model" 8 | "github.com/mahiro72/go-api-sample/domain/repository" 9 | ) 10 | 11 | type getUserPostList struct { 12 | repoUser repository.User 13 | repoPost repository.Post 14 | } 15 | 16 | func NewGetUserPostList(repoUser repository.User, repoPost repository.Post) *getUserPostList { 17 | return &getUserPostList{ 18 | repoUser: repoUser, 19 | repoPost: repoPost, 20 | } 21 | } 22 | 23 | func (uc *getUserPostList) Exec(ctx context.Context, userIDString string) ([]model.Post, error) { 24 | userID, err := model.ParseUserID(userIDString) 25 | if err != nil { 26 | if errors.Is(err, model.ErrUserIDValidation) { 27 | return []model.Post{}, ErrIDValidation 28 | } 29 | return []model.Post{}, err 30 | } 31 | 32 | // userが存在するか確認 33 | _, err = uc.repoUser.Find(ctx, userID) 34 | if err != nil { 35 | if errors.Is(err, repository.ErrNoUserFound) { 36 | return []model.Post{}, ErrNotExistsData 37 | } 38 | return []model.Post{}, err 39 | } 40 | 41 | post, err := uc.repoPost.FindAllByUserID(ctx, userID) 42 | if err != nil { 43 | return []model.Post{}, err 44 | } 45 | return post, nil 46 | } 47 | -------------------------------------------------------------------------------- /controller/handler/get_user_post_list.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "net/http" 7 | 8 | "github.com/mahiro72/go-api-sample/controller/view" 9 | "github.com/mahiro72/go-api-sample/impl/repository" 10 | "github.com/mahiro72/go-api-sample/persistence/inmemory" 11 | "github.com/mahiro72/go-api-sample/usecase" 12 | 13 | "github.com/go-chi/chi/v5" 14 | ) 15 | 16 | func GetUserPostList(w http.ResponseWriter, r *http.Request) { 17 | userIDString := chi.URLParam(r, "userID") 18 | 19 | db := inmemory.NewDatabase() 20 | uc := usecase.NewGetUserPostList(repository.NewUser(db), repository.NewPost(db)) 21 | postList, err := uc.Exec(r.Context(), userIDString) 22 | if err != nil { 23 | switch { 24 | case errors.Is(err, usecase.ErrIDValidation): 25 | w.WriteHeader(http.StatusBadRequest) 26 | return 27 | case errors.Is(err, usecase.ErrNotExistsData): 28 | w.WriteHeader(http.StatusNotFound) 29 | return 30 | default: 31 | w.WriteHeader(http.StatusInternalServerError) 32 | return 33 | } 34 | } 35 | 36 | res := view.NewPostList(postList) 37 | b, err := json.Marshal(res) 38 | if err != nil { 39 | w.WriteHeader(http.StatusInternalServerError) 40 | return 41 | } 42 | 43 | w.WriteHeader(http.StatusOK) 44 | w.Write(b) 45 | } 46 | -------------------------------------------------------------------------------- /impl/repository/post.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/mahiro72/go-api-sample/domain/model" 7 | "github.com/mahiro72/go-api-sample/domain/repository" 8 | "github.com/mahiro72/go-api-sample/persistence/inmemory" 9 | "github.com/mahiro72/go-api-sample/persistence/inmemory/dto" 10 | ) 11 | 12 | type post struct { 13 | db *inmemory.Database 14 | } 15 | 16 | func NewPost(db *inmemory.Database) *post { 17 | return &post{ 18 | db: db, 19 | } 20 | } 21 | 22 | func (repo *post) Create(ctx context.Context, post model.Post, userID model.UserID) error { 23 | repo.db.PostList = append(repo.db.PostList, dto.NewPost(post, userID)) 24 | return nil 25 | } 26 | 27 | func (repo *post) Find(ctx context.Context, postID model.PostID) (model.Post, error) { 28 | for _, p := range repo.db.PostList { 29 | post := p.ToModel() 30 | if post.ID.IsSame(postID) { 31 | return post, nil 32 | } 33 | } 34 | return model.Post{}, repository.ErrNoPostFound 35 | } 36 | 37 | func (repo *post) FindAllByUserID(ctx context.Context, userID model.UserID) ([]model.Post, error) { 38 | postList := []model.Post{} 39 | 40 | for _, p := range repo.db.PostList { 41 | if model.UserID(p.UserID).IsSame(userID) { 42 | postList = append(postList, p.ToModel()) 43 | } 44 | } 45 | return postList, nil 46 | } 47 | -------------------------------------------------------------------------------- /usecase/create_user.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/mahiro72/go-api-sample/domain/model" 8 | "github.com/mahiro72/go-api-sample/domain/repository" 9 | "github.com/mahiro72/go-api-sample/domain/service" 10 | ) 11 | 12 | type createUser struct { 13 | repoUser repository.User 14 | svcUserNameValidator service.UserNameValidator 15 | } 16 | 17 | func NewCreateUser(repoUser repository.User, svcUserNameValidator service.UserNameValidator) *createUser { 18 | return &createUser{ 19 | repoUser: repoUser, 20 | svcUserNameValidator: svcUserNameValidator, 21 | } 22 | } 23 | 24 | func (uc *createUser) Exec(ctx context.Context, name, password string) (model.User, error) { 25 | // nameが使われていないか確認 26 | used, err := uc.svcUserNameValidator.CheckAlreadyUsed(ctx, name) 27 | if err != nil { 28 | return model.User{}, err 29 | } 30 | if used { 31 | return model.User{}, ErrUserNameAlreadyUsed 32 | } 33 | 34 | user, err := model.NewUser(name, password) 35 | if err != nil { 36 | if errors.Is(err, model.ErrUserFieldsValidation) { 37 | return model.User{}, ErrFieldsValidation 38 | } 39 | return model.User{}, err 40 | } 41 | 42 | err = uc.repoUser.Create(ctx, user) 43 | if err != nil { 44 | return model.User{}, err 45 | } 46 | return user, nil 47 | } 48 | -------------------------------------------------------------------------------- /controller/handler/create_user.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "net/http" 7 | 8 | "github.com/mahiro72/go-api-sample/controller/view" 9 | "github.com/mahiro72/go-api-sample/impl/repository" 10 | "github.com/mahiro72/go-api-sample/impl/service" 11 | "github.com/mahiro72/go-api-sample/persistence/inmemory" 12 | "github.com/mahiro72/go-api-sample/usecase" 13 | ) 14 | 15 | func CreateUser(w http.ResponseWriter, r *http.Request) { 16 | var j requestCreateUser 17 | if err := json.NewDecoder(r.Body).Decode(&j); err != nil { 18 | w.WriteHeader(http.StatusBadRequest) 19 | return 20 | } 21 | 22 | db := inmemory.NewDatabase() 23 | repoUser := repository.NewUser(db) 24 | uc := usecase.NewCreateUser(repoUser, service.NewUserNameValidator(repoUser)) 25 | user, err := uc.Exec(r.Context(), j.Name, j.Password) 26 | if err != nil { 27 | if errors.Is(err, usecase.ErrFieldsValidation) { 28 | w.WriteHeader(http.StatusBadRequest) 29 | return 30 | } 31 | w.WriteHeader(http.StatusInternalServerError) 32 | return 33 | } 34 | 35 | res := view.NewUser(user) 36 | b, err := json.Marshal(res) 37 | if err != nil { 38 | w.WriteHeader(http.StatusInternalServerError) 39 | return 40 | } 41 | 42 | w.WriteHeader(http.StatusCreated) 43 | w.Write(b) 44 | } 45 | 46 | type requestCreateUser struct { 47 | Name string `json:"name"` 48 | Password string `json:"password"` 49 | } 50 | -------------------------------------------------------------------------------- /usecase/create_user_post.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/mahiro72/go-api-sample/domain/model" 8 | "github.com/mahiro72/go-api-sample/domain/repository" 9 | "github.com/mahiro72/go-api-sample/domain/service" 10 | ) 11 | 12 | type createUserPost struct { 13 | repoPost repository.Post 14 | svcUserRegistrationChecker service.UserRegistrationChecker 15 | } 16 | 17 | func NewCreateUserPost(repoPost repository.Post, svcUserRegistrationChecker service.UserRegistrationChecker) *createUserPost { 18 | return &createUserPost{ 19 | repoPost: repoPost, 20 | svcUserRegistrationChecker: svcUserRegistrationChecker, 21 | } 22 | } 23 | 24 | func (uc *createUserPost) Exec(ctx context.Context, title, content, name, password string) (model.Post, error) { 25 | // userが登録されているか nameとpasswordを使って確認 26 | userID, err := uc.svcUserRegistrationChecker.CheckUserRegistration(ctx, name, password) 27 | if err != nil { 28 | if errors.Is(err, service.ErrNoUserFound) { 29 | return model.Post{}, ErrNotExistsData 30 | } 31 | return model.Post{}, err 32 | } 33 | 34 | post, err := model.NewPost(title, content) 35 | if err != nil { 36 | if errors.Is(err, model.ErrPostFieldsValidation) { 37 | return model.Post{}, ErrFieldsValidation 38 | } 39 | return model.Post{}, err 40 | } 41 | 42 | err = uc.repoPost.Create(ctx, post, userID) 43 | if err != nil { 44 | return model.Post{}, err 45 | } 46 | return post, nil 47 | } 48 | -------------------------------------------------------------------------------- /impl/repository/user.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/mahiro72/go-api-sample/domain/model" 7 | "github.com/mahiro72/go-api-sample/domain/repository" 8 | "github.com/mahiro72/go-api-sample/persistence/inmemory" 9 | "github.com/mahiro72/go-api-sample/persistence/inmemory/dto" 10 | ) 11 | 12 | type user struct { 13 | db *inmemory.Database 14 | } 15 | 16 | func NewUser(db *inmemory.Database) repository.User { 17 | return &user{ 18 | db: db, 19 | } 20 | } 21 | 22 | func (repo *user) Create(ctx context.Context, user model.User) error { 23 | repo.db.UserList = append(repo.db.UserList, dto.NewUser(user)) 24 | return nil 25 | } 26 | 27 | func (repo *user) Find(ctx context.Context, userID model.UserID) (model.User, error) { 28 | for _, u := range repo.db.UserList { 29 | user := u.ToModel() 30 | if user.ID.IsSame(userID) { 31 | return user, nil 32 | } 33 | } 34 | return model.User{}, repository.ErrNoUserFound 35 | } 36 | 37 | func (repo *user) FindByName(ctx context.Context, name string) (model.User, error) { 38 | for _, u := range repo.db.UserList { 39 | user := u.ToModel() 40 | if name == user.Name { 41 | return user, nil 42 | } 43 | } 44 | return model.User{}, repository.ErrNoUserFound 45 | } 46 | 47 | func (repo *user) FindByNameAndPassword(ctx context.Context, name, password string) (model.User, error) { 48 | for _, u := range repo.db.UserList { 49 | user := u.ToModel() 50 | if name == user.Name && password == user.Password { 51 | return user, nil 52 | } 53 | } 54 | return model.User{}, repository.ErrNoUserFound 55 | } 56 | -------------------------------------------------------------------------------- /controller/handler/create_user_post.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "net/http" 7 | 8 | "github.com/mahiro72/go-api-sample/controller/view" 9 | "github.com/mahiro72/go-api-sample/impl/repository" 10 | "github.com/mahiro72/go-api-sample/impl/service" 11 | "github.com/mahiro72/go-api-sample/persistence/inmemory" 12 | "github.com/mahiro72/go-api-sample/usecase" 13 | ) 14 | 15 | func CreateUserPost(w http.ResponseWriter, r *http.Request) { 16 | // 認証情報取得 17 | name, password, ok := r.BasicAuth() 18 | if !ok { 19 | w.WriteHeader(http.StatusUnauthorized) 20 | return 21 | } 22 | 23 | var j requestCreateUserPost 24 | if err := json.NewDecoder(r.Body).Decode(&j); err != nil { 25 | w.WriteHeader(http.StatusBadRequest) 26 | return 27 | } 28 | 29 | db := inmemory.NewDatabase() 30 | svcUserRegistrationChecker := service.NewUserRegistrationChecker(repository.NewUser(db)) 31 | uc := usecase.NewCreateUserPost(repository.NewPost(db), svcUserRegistrationChecker) 32 | post, err := uc.Exec(r.Context(), j.Title, j.Content, name, password) 33 | if err != nil { 34 | switch { 35 | case errors.Is(err, usecase.ErrFieldsValidation): 36 | w.WriteHeader(http.StatusBadRequest) 37 | return 38 | case errors.Is(err, usecase.ErrNotExistsData): 39 | w.WriteHeader(http.StatusNotFound) 40 | return 41 | default: 42 | w.WriteHeader(http.StatusInternalServerError) 43 | return 44 | } 45 | } 46 | 47 | res := view.NewPost(post) 48 | b, err := json.Marshal(res) 49 | if err != nil { 50 | w.WriteHeader(http.StatusInternalServerError) 51 | return 52 | } 53 | 54 | w.WriteHeader(http.StatusCreated) 55 | w.Write(b) 56 | } 57 | 58 | type requestCreateUserPost struct { 59 | Title string `json:"title"` 60 | Content string `json:"content"` 61 | } 62 | -------------------------------------------------------------------------------- /domain/model/user.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "regexp" 5 | "time" 6 | 7 | "github.com/mahiro72/go-api-sample/utils" 8 | 9 | "github.com/google/uuid" 10 | ) 11 | 12 | type UserID string 13 | 14 | func NewUserID() UserID { 15 | return UserID(uuid.NewString()) 16 | } 17 | 18 | func (u UserID) String() string { 19 | return string(u) 20 | } 21 | 22 | func (u UserID) IsSame(other UserID) bool { 23 | return u.String() == other.String() 24 | } 25 | 26 | func ParseUserID(userIDString string) (UserID, error) { 27 | userID, err := uuid.Parse(userIDString) 28 | if err != nil { 29 | return "", ErrUserIDValidation 30 | } 31 | return UserID(userID.String()), nil 32 | } 33 | 34 | type User struct { 35 | //ユーザーを一意に識別するID 36 | ID UserID 37 | //ユーザーの名前 38 | Name string 39 | //ユーザーのパスワード 40 | Password string 41 | //ユーザーの作成日 42 | CreatedAt time.Time 43 | } 44 | 45 | func NewUser(name, password string) (User, error) { 46 | if !(4 <= len(name) && len(name) <= 20) { 47 | return User{}, ErrUserFieldsValidation 48 | } 49 | 50 | if !(4 <= len(password) && len(password) <= 20) { 51 | return User{}, ErrUserFieldsValidation 52 | } 53 | 54 | // 大文字小文字アルファベット, 数字のみを許可 55 | regexPattern := `^[A-Za-z0-9]+$` 56 | 57 | // nameのvalidation 58 | ok, err := regexp.MatchString(regexPattern, name) 59 | if err != nil { 60 | return User{}, err 61 | } 62 | if !ok { 63 | return User{}, ErrUserFieldsValidation 64 | } 65 | 66 | // passwordのvalidation 67 | ok, err = regexp.MatchString(regexPattern, password) 68 | if err != nil { 69 | return User{}, err 70 | } 71 | if !ok { 72 | return User{}, ErrUserFieldsValidation 73 | } 74 | 75 | // nameをsaltとして使いhash化する 76 | hashPwd := utils.CreateHashFromString(name + password) 77 | 78 | return User{ 79 | ID: NewUserID(), 80 | Name: name, 81 | Password: hashPwd, 82 | CreatedAt: time.Now(), 83 | }, nil 84 | } 85 | -------------------------------------------------------------------------------- /impl/repository/user_test.go: -------------------------------------------------------------------------------- 1 | package repository_test 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "testing" 7 | "time" 8 | 9 | "github.com/mahiro72/go-api-sample/domain/model" 10 | "github.com/mahiro72/go-api-sample/domain/repository" 11 | repositoryImpl "github.com/mahiro72/go-api-sample/impl/repository" 12 | "github.com/mahiro72/go-api-sample/persistence/inmemory" 13 | ) 14 | 15 | func TestUser_Create(t *testing.T) { 16 | t.Run("normal", func(t *testing.T) { 17 | inmemory.ResetDatabase() 18 | db := inmemory.NewDatabase() 19 | 20 | repo := repositoryImpl.NewUser(db) 21 | err := repo.Create(context.Background(), model.User{ 22 | ID: "xxxx", 23 | Name: "xxxx", 24 | CreatedAt: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), 25 | }) 26 | if err != nil { 27 | t.Fatal(err) 28 | } 29 | }) 30 | } 31 | 32 | func TestUser_Find(t *testing.T) { 33 | t.Run("normal", func(t *testing.T) { 34 | inmemory.ResetDatabase() 35 | db := inmemory.NewDatabase() 36 | // データの用意 37 | err := repositoryImpl.NewUser(db).Create(context.Background(), model.User{ 38 | ID: "xxxx", 39 | Name: "xxxx", 40 | CreatedAt: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), 41 | }) 42 | if err != nil { 43 | panic(err) 44 | } 45 | 46 | repo := repositoryImpl.NewUser(db) 47 | _, err = repo.Find(context.Background(), "xxxx") 48 | if err != nil { 49 | t.Fatal(err) 50 | } 51 | }) 52 | 53 | t.Run("failure no user found", func(t *testing.T) { 54 | inmemory.ResetDatabase() 55 | db := inmemory.NewDatabase() 56 | // データの用意 57 | err := repositoryImpl.NewUser(db).Create(context.Background(), model.User{ 58 | ID: "xxxx", 59 | Name: "xxxx", 60 | CreatedAt: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), 61 | }) 62 | if err != nil { 63 | panic(err) 64 | } 65 | 66 | repo := repositoryImpl.NewUser(db) 67 | _, err = repo.Find(context.Background(), "yyyy") 68 | if !errors.Is(err, repository.ErrNoUserFound) { 69 | t.Fatal(err) 70 | } 71 | }) 72 | } 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go API Sample 2 | 3 | GoのAPIのサンプルリポジトリです。 4 | 5 | 気になる点や改善点などありましたらissueを立てたり、気軽に[@mahiro0x00](https://x.com/mahiro0x00)までご連絡ください。 6 | 7 | ## 内容 8 | 9 | シンプルな掲示板サービス 10 | 11 |
12 | 13 | ※ セキュリティ周りや永続化周りはふわっとしか書いてません。 14 | 15 | ※ アーキテクチャや責務の切り分けについてをメインに書いてるリポジトリです。 16 | 17 | 18 | ## アーキテクチャ構成 19 | ``` 20 | . 21 | ├── controller 22 | │ ├── handler //APIエンドポイントに対応するハンドラー 23 | │ ├── router //serverのエントリーポイント 24 | │ └── view //response用の構造体に変換する 25 | ├── domain 26 | │ ├── model //ドメインやドメインロジック 27 | │ ├── repository //永続化のインターフェース 28 | │ └── service //ドメインロジックのインターフェース 29 | ├── impl 30 | │ ├── repository //domain/repositoryの具体的な実装 31 | │ └── service //domain/serviceの具体的な実装 32 | ├── main.go //このアプリケーションのエントリーポイント 33 | ├── persistence 34 | │ └── inmemory 35 | │ ├── database.go //inmemoryで永続化するロジック 36 | │ └── dto //modelをinmemoryで扱うデータ構造に変換する 37 | ├── usecase //エンドポイントと1:1に対応するビジネスロジック 38 | └── utils //便利関数など 39 | ``` 40 | 41 | 42 | ## Server起動 43 | 44 | ``` 45 | go run main.go 46 | ``` 47 | 48 | Serverのhealthを確認 49 | ``` 50 | curl http://localhost:8080/health 51 | ``` 52 | 53 | response 54 | ``` 55 | health ok! 56 | ``` 57 | 58 | ## 機能 59 | 60 | 機能は以下 61 | 62 | 63 | - ユーザー登録 64 | - ユーザー取得 65 | - ユーザーの投稿作成 66 | - Basic認証が必要 67 | - ユーザーの投稿一覧取得 68 | 69 |
70 | 71 | ### 機能の動作確認 72 | 73 | ユーザー登録 74 | 75 | ``` 76 | curl -X POST http://localhost:8080/users --data '{"name":"hoge","password":"hoge"}' 77 | ``` 78 | 79 | response 80 | ```json 81 | {"id":"96b4bad9-fb9c-4cd0-b518-b9529b0112f8","name":"hoge","created_at":"2023-10-21T15:56:05.968516+09:00"} 82 | ``` 83 | 84 |
85 | 86 | ユーザー取得 87 | 88 | ``` 89 | curl http://localhost:8080/users/{userID} 90 | ``` 91 | 92 | response 93 | ```json 94 | {"id":"96b4bad9-fb9c-4cd0-b518-b9529b0112f8","name":"hoge","created_at":"2023-10-21T15:56:05.968516+09:00"} 95 | ``` 96 | 97 |
98 | 99 | ユーザーの投稿作成 100 | 101 | Basic認証を使う 102 | 103 | ``` 104 | curl -X POST http://localhost:8080/users/{userID}/posts \ 105 | --header 'Authorization: Basic xxxx' \ 106 | --data '{"title":"hello","content":"hoge"}' 107 | ``` 108 | 109 | response 110 | ```json 111 | {"id":"cf7fe1cc-0c9b-4bdf-9b7b-102351a3c338","title":"hello","content":"hoge","created_at":"2023-10-21T16:04:48.219667+09:00"} 112 | ``` 113 | 114 |
115 | 116 | ユーザーの投稿一覧取得 117 | 118 | ``` 119 | curl http://localhost:8080/users/{userID}/posts/list 120 | ``` 121 | 122 | response 123 | ```json 124 | [{"id":"03beabab-e6c3-43c5-97eb-2f7362f18b98","title":"hello","content":"hoge","created_at":"2023-10-21T16:04:41.74259+09:00"},{"id":"4d0055f5-bfd0-445e-b3c2-d5de00ecea3e","title":"hello","content":"hoge","created_at":"2023-10-21T16:04:46.515345+09:00"}] 125 | ``` 126 | --------------------------------------------------------------------------------