├── 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 |
--------------------------------------------------------------------------------