Golang (net/http)とFirestoreで簡単なREST APIを作ってみた。
- 1. 概要
- 2. 環境設定
- 2. 全体像
- 3. controller層実装
- 4. application層の実装
- 5. infrastructrure層の実装
- 6. API サーバ起動と動作確認
- 7. 動作確認
- 8. まとめ
1. 概要
最近GolangでAPIの開発をする機会があり、GinなどのAPIフレームワークを使わずに標準ライブラリのnet/httpのみで開発をおこないました。 フレームワークの場合はGETやPOSTのハンドリングをよしなにやってくれますが、net/httpでは明示的にhttp methodをハンドリングする処理を書く必要がありました。
今回はユーザのCRUD機能を持つだけの簡単なREST APIのサンプルプログラムを元に、 上記開発の機会から学んだことをアウトプットしようというモチベーションで本記事を書きました。
今回の内容
今回作るもの
注意事項
まだ筆者はGolangの経験が浅いのでerror handlingやlogなど実務上甘い点が多々あるかもしれないのでアドバイスありましたらコメントいただければと思いますmm
それとソースコード一部のみしか抜粋して記事に載せていないので適宜下記のサンプルコードと見合わせてご覧いただくといいと思います。
実際に筆者が作ったサンプルコードはこちら
2. 環境設定
project設定
module pathを指定してgo mod initを実行し
# 例 go mod init github.com/MoriTomo7315/go-user-rest-api go mod init github.com/<your_git_repo_name>/<project_name>
必要なモジュールをgetする
go get cloud.google.com/go/firestore
go get firebase.google.com/go
go get github.com/joho/godotenv # .env使用するためのモジュール
今回作ろうとしているAPIのディレクトリ構造
下記のようにDDD(ドメイン駆動開発)のレイヤードアーキテクチャを意識して設計しました。
. ├── Dockerfile ├── README.md ├── application # アプリケーション層 (ビジネスロジックを書くところ) │ ├── user.go │ ├── user_test.go │ └── util │ ├── error.go │ └── response.go ├── controller # コントローラー層 (http requestのハンドリングの処理を書くところ) │ └── user.go ├── domain # ドメイン層 │ ├── define │ │ └── error.go │ ├── model │ │ ├── error.go │ │ ├── response.go │ │ └── user.go │ └── repository │ └── firestore.go ├── firebase_credential.json ├── go.mod ├── go.sum ├── infrastructure # インフラ層 │ └── persistence │ ├── firestore.go │ └── firestore_test.go └── main.go
2. 全体像
3. controller層実装
controller層はhttp requestをclientから受け取るときに、このリクエストがGET methodであれば、User情報取得の処理に割り振り、POST methodであればUser作成、更新処理に割り振る、といったような制御を目的とした層です。なのでリクエストを受け取ったら最初に処理がおこなわれる部分となります。もちろんこのcontroller層を用いてGET, POST以外のmethodを受け付けないといったような制御も可能となります(本来はweb serverで制御すべきかもしれないがアプリ側でも制御はできる)
controller層のインターフェース
controller層は2つのインターフェースを持ち、main.go (serverにあたるもの)によって使用されます。
https://github.com/MoriTomo7315/go-user-rest-api/blob/master/controller/user.go#L11-L13
type UserController interface { HandlerHttpRequest(w http.ResponseWriter, r *http.Request) HandlerHttpRequestWithParameter(w http.ResponseWriter, r *http.Request) }
HandlerHttpRequestとHandlerHttpRequestWithParameterの違い
HandlerHttpRequest
とHandlerHttpRequestWithParameter
の2つのメソッドを用意している理由は、
例えばユーザ情報を全取得したいときに下記のようにリクエストされることが考えられます。
HandlerHttpRequestWithParameterだけだと、例えばlocalhost:/50001/api/usersでアクセスするとそのようなurlをハンドリングする処理はないためエラーとなってしまいます。
なのでControllerの各2つのmethodは下記のようにHTTP Requestを捌いています。
- GET /api/users, POST /api/users
- HandlerHttpRequest()
- GET /api/users/, GET /api/users/:user_id, POST /api/users/, POST/api/users/:user_id, DELETE /api/users/:user_id
- HandlerHttpRequestWithParameter()
詳細な実装
https://github.com/MoriTomo7315/go-user-rest-api/blob/master/controller/user.go#L26-L89
func (uc *userController) HandlerHttpRequest(w http.ResponseWriter, r *http.Request) { log.Printf("INFO [/api/users] START ===========") switch r.Method { case http.MethodGet: /* 全Userを取得する */ uc.userApplication.GetUsers(w, r) case http.MethodPost: /* Userを作成する */ uc.userApplication.CreateUser(w, r) default: /* GET, POST以外のhttp methodは許可しない */ w.WriteHeader(405) } log.Printf("INFO [/api/users] END ===========") } func (uc *userController) HandlerHttpRequestWithParameter(w http.ResponseWriter, r *http.Request) { log.Printf("INFO [/api/users/] START ===========") userId := strings.TrimPrefix(r.URL.Path, "/api/users/") switch r.Method { case http.MethodGet: if len(userId) == 0 { /* /api/users/でリクエストが来た場合は/api/userと同じ処理をする(リダイレクト的役割) */ uc.userApplication.GetUsers(w, r) } else { /* /api/users/:userIdでリクエストが来た場合はidがuserIdのuser情報を返す */ uc.userApplication.GetUserById(w, r, userId) } case http.MethodPost: if len(userId) == 0 { /* /api/users/でリクエストが来た場合は/api/userと同じ処理をする(リダイレクト的役割) */ uc.userApplication.CreateUser(w, r) } else { /* /api/users/:userIdでリクエストが来た場合はidがuserIdのuser情報を更新する */ uc.userApplication.UpdateUser(w, r, userId) } case http.MethodDelete: if len(userId) == 0 { w.WriteHeader(400) } else { uc.userApplication.DeleteUser(w, r, userId) } default: /* GET, POST, DELETE以外のhttp methodは許可しない */ w.WriteHeader(405) } log.Printf("INFO [/api/users/] END ===========") }
http methodのハンドリング
Controller層はnet/httpのHttpRequest型でhttp requestを受け取ります。 各関数はhttp requestを r として受けとっており、"r.Method" でhttp methodが何かを参照し、どのビジネスロジックに振り分けるかを判断します。
http methodの判定にはnet/httpの定数に、http.MethodGet, http.MethodPostなどが用意されているため、明示的に"GET", "POST"と書くのではなく、net/httpモジュールの定数を使用することが好ましそうです。
/api/users/:user_idのuser_idの取り出しについて
下記がurlからuser_idを取り出す処理に該当しますが、stringsモジュールというもののTrimPrefixというメソッドを使用しています。 Prefix(接頭辞)をトリムするという、文字通りのメソッドで、/api/users/hogehoge から/api/users/をトリムして、hogehogeがuserIdに代入されることとなります。
userId := strings.TrimPrefix(r.URL.Path, "/api/users/")
4. application層の実装
application層は受け取ったhttp request bodyの詳細なバリデーションチェックや、ユーザの存在チェック、firestoreから取得したユーザデータをレスポンスモデルにマッピングするといったビジネスロジックを実装しています。
5節のinfrastructure層で詳細は後述しますが、
application層からユーザ情報を取得する際にはdomain/repository/firestore.go
で定義するFirestoreRepositoryのメソッドを使用しています。FirestoreRepositoryにfirestoreへのユーザ情報取得や作成のためのクエリのinterfaceを持っているイメージです。
https://github.com/MoriTomo7315/go-user-rest-api/blob/master/application/user.go#L23-L25
type userApplication struct { firestoreRepository repository.FirestoreRepository }
FirestoreRepositoryの詳細はこちらをご覧ください。
ビジネスロジックの一覧
application層のインターフェースとして下記を持っています。 それぞれの役割については名前から理解できると思うので省略します。
https://github.com/MoriTomo7315/go-user-rest-api/blob/master/application/user.go#L14-L21
// インターフェース type UserApplication interface { GetUsers(w http.ResponseWriter, r *http.Request) GetUserById(w http.ResponseWriter, r *http.Request, userId string) CreateUser(w http.ResponseWriter, r *http.Request) UpdateUser(w http.ResponseWriter, r *http.Request, userId string) DeleteUser(w http.ResponseWriter, r *http.Request, userId string) }
全ユーザ情報取得
全ユーザ情報を取得するビジネスロジックはGetUsers(w http.ResponseWriter, r *http.Request)
に実装しました。
GET /api/users or /api/users/のときに、このインターフェースはController層から使用されます。
このインターフェースの実装は以下のようになります。
https://github.com/MoriTomo7315/go-user-rest-api/blob/master/application/user.go#L34-L47
// ユーザ一覧取得 func (ua userApplication) GetUsers(w http.ResponseWriter, r *http.Request) { log.Printf("INFO [GetUsers] Application logic start") // firestoreからユーザ情報を取得する users, err := ua.firestoreRepository.GetUsers() // エラーチェック if err != nil { util.CreateErrorResponse(w, err, "") return } // エラーがない場合はレスポンスを作成する resModel := util.GetResponse(http.StatusOK, "ユーザ情報取得に成功しました。", int64(len(users)), users) res, _ := json.Marshal(resModel) w.Header().Set("Content-Type", "application/json") w.Write(res) }
特定のユーザ情報取得
特定のユーザ情報を取得するビジネスロジックはGetUserById(w http.ResponseWriter, r *http.Request, userId string)
に実装しました。
GET /api/users/:user_idのときに、このインターフェースはController層から使用されます。
このインターフェースの実装は以下のようになります。
https://github.com/MoriTomo7315/go-user-rest-api/blob/master/application/user.go#L49-L63
func (ua userApplication) GetUserById(w http.ResponseWriter, r *http.Request, userId string) { log.Printf("INFO [GetUserById] Application logic start") // firestoreからuserIdを指定してユーザ情報を取得する user, err := ua.firestoreRepository.GetUserById(userId) // エラーチェック if err != nil { util.CreateErrorResponse(w, err, userId) return } // エラーがない場合はレスポンスを作成する users := []*model.User{user} resModel := util.GetResponse(http.StatusOK, "ユーザ情報取得に成功しました。", int64(len(users)), users) res, _ := json.Marshal(resModel) w.Header().Set("Content-Type", "application/json") w.Write(res) }
ユーザ情報の作成
ユーザ情報を作成するビジネスロジックはCreateUser(w http.ResponseWriter, r *http.Request)
に実装しました。
POST /api/users or /api/users/のときに、このインターフェースはController層から使用されます。
このインターフェースの実装は以下のようになります。
https://github.com/MoriTomo7315/go-user-rest-api/blob/master/application/user.go#L65-L82
func (ua userApplication) CreateUser(w http.ResponseWriter, r *http.Request) { log.Printf("INFO [CreateUser] Application logic start") // http request bodyを取得 body, _ := ioutil.ReadAll(r.Body) // http request bodyをUserModelにmapping var user *model.User json.Unmarshal(body, &user) // firestore上にユーザ情報を登録 err := ua.firestoreRepository.CreateUser(user) // エラーチェック if err != nil { util.CreateErrorResponse(w, err, "") return } // エラーがない場合はレスポンスを作成する resModel := util.GetResponse(http.StatusCreated, "作成に成功しました。", 0, nil) res, _ := json.Marshal(resModel) w.Header().Set("Content-Type", "application/json") w.Write(res) }
ユーザ情報の更新
ユーザ情報を更新するビジネスロジックはUpdateUser(w http.ResponseWriter, r *http.Request, userId string)
に実装しました。
POST /api/users/:user_idのときに、このインターフェースはController層から使用されます。
このインターフェースの実装は以下のようになります。
https://github.com/MoriTomo7315/go-user-rest-api/blob/master/application/user.go#L84-L108
func (ua userApplication) UpdateUser(w http.ResponseWriter, r *http.Request, userId string) { log.Printf("INFO [UpdateUser] Application logic start") // user存在 チェック _, err := ua.firestoreRepository.GetUserById(userId) if err != nil { util.CreateErrorResponse(w, err, userId) return } // http request bodyを取得 body, _ := ioutil.ReadAll(r.Body) var newUser *model.User // request bodyはjson形式のためencoding/jsonでUserModelにマッピングできる json.Unmarshal(body, &newUser) newUser.Id = userId // firestore上のユーザ情報を更新 err = ua.firestoreRepository.UpdateUser(newUser) if err != nil { util.CreateErrorResponse(w, err, userId) return } resModel := util.GetResponse(http.StatusNoContent, "更新に成功しました。", 0, nil) res, _ := json.Marshal(resModel) w.Header().Set("Content-Type", "application/json") w.Write(res) }
ユーザ情報の削除
ユーザ情報を更新するビジネスロジックはDeleteUser(w http.ResponseWriter, r *http.Request, userId string)
に実装しました。
DELETE /api/users/:user_idのときに、このインターフェースはController層から使用されます。
このインターフェースの実装は以下のようになります。
https://github.com/MoriTomo7315/go-user-rest-api/blob/master/application/user.go#L110-L130
func (ua userApplication) DeleteUser(w http.ResponseWriter, r *http.Request, userId string) { log.Printf("INFO [DeleteUser] Application logic start") // user存在 チェック _, err := ua.firestoreRepository.GetUserById(userId) if err != nil { util.CreateErrorResponse(w, err, userId) return } // firestore上のユーザ情報を削除 err = ua.firestoreRepository.DeleteUser(userId) if err != nil { util.CreateErrorResponse(w, err, userId) return } resModel := util.GetResponse(http.StatusNoContent, "削除に成功しました。", 0, nil) res, _ := json.Marshal(resModel) w.Header().Set("Content-Type", "application/json") w.Write(res) }
5. infrastructrure層の実装
infrastructure層ではdomain層がapplication層に提供しているinterfaceの詳細を実装する役割があります。 今回はdomain層のrepositoryにapplication層がfirestoreを使用できるようにinterfaceを用意しているため firestoreのアクセスに関する処理をinfrastructure層に実装しました。
firestoreについて
まずfirestoreとはGoogleが提供しているNoSQL型のcloud databaseです。
詳細は上記ドキュメントに書かれてますので割愛しますが、 公式からGolang用のfirestoreのためのSDKが公開されていますし、公式DocsにもGolangでの使用例(他言語も対応)が記載されています。
firestoreを使用するための準備
- まずFirebase コンソールにアクセス
- "設定" > "サービス アカウント" を開く
- "新しい秘密鍵の生成"をクリックし、"キーを生成"をクリックして確定
- キーを含む JSON ファイルをproject rootでもどこでもいいので保存 (大切に保管) gitなどpublicなところに後悔しないように.gitignoreの設定を忘れずに!
.envファイルを用意します。筆者の場合はlocal, dev, prodで環境変数を分けることを意識したので.env.localにしています。 creadential情報を持つJSONファイルと同じ階層に.envをセットしてください。
.envの中身
GOOGLE_APPLICATION_CREDENTIALS=./<your_file_name>.json
GOOGLE_APPLICATION_CREDENTIALS
という環境変数名は必ず同じにしてください。
というのも、firestoreを初期化は
// .envのGOOGLE_APPLICATION_CREDENTIALSから暗黙的に設定を読み取る app, err := firebase.NewApp(ctx, nil)
のようなコードでおこなわれるのですが、このとき実行環境に環境変数GOOGLE_APPLICATION_CREDENTIALS
が設定されている場合は
NewApp
時にファイルのパスを指定しなくても暗黙的にロードしてくれるという動きのためです。
これでGolangからfirestoreにアクセスするために必要なリソースは揃いました。 詳細な実装はgithub repositoryを追っていただければと思います。
全ユーザを取得するクエリ
下記のように全ユーザを取得するための実装しました。
この実装でやりたいこととしては、
UserModelにUid(ドキュメントを識別するユニークなID)とデータの中身(名前と都道府県)をセットして、[]UserModel型
のリストをapplication層に返してあげることです。(https://github.com/MoriTomo7315/go-user-rest-api/blob/master/domain/model/user.go)
// firestoreから全ユーザの情報を取得する func (f *firestoreClient) GetUsers() (users []*model.User, err error) { log.Printf("INFO [GetUsers] connecting firestore start.") // ① init firestore client ctx := context.Background() client, err := initFireStoreClient(ctx) defer client.Close() if err != nil { log.Printf("ERROR firestore clientの初期化に失敗 err=%v", err) return nil, define.SYSTEM_ERR } // ② "users"コレクションからすべてのドキュメントを取得するIteratorを取得 iter := client.Collection("users").Documents(ctx) for { // ③ 各ドキュメントをUserModelにマッピングし、usersリストに詰める userDocSnap, err := iter.Next() if err == iterator.Done { break } if err != nil { log.Printf("ERROR firestoreからusersコレクションの検索に失敗 err=%v", err) return nil, define.NOT_FOUND_USER } // Uidをmap[string]interface{}に含める userData := userDocSnap.Data() userData["id"] = userDocSnap.Ref.ID // map[string]interface{} →json []byte -> *model.BookingModel jsonuserData, _ := json.Marshal(userData) var user *model.User json.Unmarshal(jsonuserData, &user) users = append(users, user) } log.Printf("INFO [GetUsers] connecting firestore end.") return users, nil }
firestoreへのアクセス自体はSDKを使用しているので簡単ですが、 戸惑うポイントが返り値です。 この返り値についてはfirestore packageのドキュメントを参照すれば簡単に解決できます。
iter := client.Collection("users").Documents(ctx)
の部分で実際にUsersコレクション(RDBでいうところのテーブル)からUserドキュメント(RDBでいうところのレコード)を取得しますが、
iter
という返り値はDocumentIterator型
であり、複数のUserドキュメントを保持しているものと考えます。
次にiter.Next()
で1つずつDocumentSnapshot型のuserDocSnap
とerror型のerr
を受け取っています。
DocumentSnapshot型にはRefフィールドから参照できるDocumentRef型
とデータ参照のためのData()
関数が存在するのでこれらを活用させていただきます。
- uidはDocumentSnapshot.Ref.ID (DocumentSnapshot→DocumentRef→フィールドのID)
- データの中身はDocumentSnapshot.Data()
ここで注意が必要なのはDocumentSnapshot.Data()
の返り値がmap[string]interface{}型
ということです。
今回のAPIの例だと、返り値は{"name": "テスト名前", "prefecture": "東京"}
のようになるのですが、"テスト名前"や"東京"に当たる部分がどのような形で返ってくるかわからないためstring型ではなくinterface型で返ってくるのです。
そのため例えば、名前だけ参照したい場合には
name string = DocumentSnapshot.Data()["name"].(string)
といったように.(string)
で明示的に文字列型に直してあげないといけないです。
でも今回の場合はまとめてmap[string]interface{}型をUserModel型に変換するので、
encoding/json
のMarshalを使用すればUserModel型への構造体に簡単にマッピングすることができます。
interface型について学びたい方はこちらへどうぞ。
userIdを指定して特定のユーザを取得するクエリ
下記のようにuserId(UserドキュメントのUid)を指定してユーザを取得するための実装しました。
先ほどの全ユーザを取得する時とは異なり、Uidをつかってfirestoreから取得するドキュメントは一意に定まっています。
なので.Doc(id).Get(ctx)
といった感じにfirestoreのSDKを使用します。
この場合の返り値は先ほども紹介したDocumentSnap型
なので、同様にDocumentSnap.Ref.ID
とDocumentSnap.Data()
を用いてUserModel
に必要なデータを取得します。
// firestoreからユーザの情報を取得する func (f *firestoreClient) GetUserById(id string) (user *model.User, err error) { log.Printf("INFO [GetUserById] connecting firestore start. id=%s", id) // init firestore client ctx := context.Background() client, err := initFireStoreClient(ctx) defer client.Close() if err != nil { log.Printf("ERROR firestore clientの初期化に失敗 err=%v", err) return nil, define.SYSTEM_ERR } // Uidを指定してUserドキュメントを取得 userDocSnap, err := client.Collection("users").Doc(id).Get(ctx) if err != nil { log.Printf("ERROR firestoreからusersコレクションの検索に失敗 id=%s, err=%v", id, err) return nil, define.NOT_FOUND_USER } // userドキュメントの中身を返却 // Uidをmap[string]interface{}に含める userData := userDocSnap.Data() userData["id"] = userDocSnap.Ref.ID // map[string]interface{} →json []byte -> *model.BookingModel jsonuserData, _ := json.Marshal(userData) json.Unmarshal(jsonuserData, &user) log.Printf("INFO [GetUserById] connecting firestore end. id=%s", id) return user, nil }
ユーザをfirestoreに登録するクエリ
ユーザ作成する場合はfirestoreのusersコレクションにドキュメントとして登録します。
firestoreにドキュメントとして登録する際はAdd
メソッドを使用します。引数には
map[string]interface{}型のデータを与えます。今回はユーザを作成したいのでname, prefecture, createdAt, updateAtを与えます。
createdAtは作成日を意味するので現在のtimestampをtime.Now()で指定します。updatedAtは更新された日を意味しますが作成のため、nil
にしておきます。
// firestoreにユーザの情報を作成する func (f *firestoreClient) CreateUser(user *model.User) (err error) { log.Printf("INFO [CreateUser] connecting firestore start. name=%v", user) // init firestore client ctx := context.Background() client, err := initFireStoreClient(ctx) defer client.Close() if err != nil { log.Printf("ERROR firestore clientの初期化に失敗 err=%v", err) return define.SYSTEM_ERR } _, _, err = client.Collection("users").Add(ctx, map[string]interface{}{ "name": user.Name, "prefecture": user.Prefecture, "createdAt": time.Now(), "updatedAt": nil, }) if err != nil { log.Printf("ERROR firestoreのusersコレクションの作成に失敗 err=%v", err) return define.FAILED_CREATE_USER } log.Printf("INFO [CreateUser] connecting firestore end.") return nil }
userIdを指定してユーザ情報を更新するクエリ
ユーザを更新する場合は、firestoreのusersコレクションのドキュメントを更新します。
このときDoc(uid).Set
メソッドを使用します。引数にはAdd
メソッドと同様にmap[string]interface{}を用います。
application層からこのUpdateUserが呼び出される時に更新後のデータを持つ*model.User
型のuser
を受け取っているので、
それを用いてmap[string]interface{}をSet
メソッドに与えます。
このとき、updatedAtの値はtime.Now()
にして更新の日時を与えるようにしています。
// firestoreのユーザ情報を更新する func (f *firestoreClient) UpdateUser(user *model.User) (err error) { log.Printf("INFO [UpdateUser] connecting firestore start. user=%v", user) // init firestore client ctx := context.Background() client, err := initFireStoreClient(ctx) defer client.Close() if err != nil { log.Printf("ERROR firestore clientの初期化に失敗 err=%v", err) return define.SYSTEM_ERR } _, err = client.Collection("users").Doc(user.Id).Set(ctx, map[string]interface{}{ "name": user.Name, "prefecture": user.Prefecture, "createdAt": user.CreatedAt, "updatedAt": time.Now(), }) if err != nil { log.Printf("ERROR firestoreのusersコレクションの更新に失敗 id=%s, err=%v", user.Id, err) return define.FAILED_UPDATE_USER } log.Printf("INFO [UpdateUser] connecting firestore end.") return nil }
userIdを指定してユーザ情報を削除するクエリ
ユーザ削除の場合、firestore上のuserコレクションのドキュメントも削除する。
ドキュメント削除にはDoc(uid).Delete
メソッドを使用する。特に難しくはないはず。
// firestoreのユーザ情報を削除する func (f *firestoreClient) DeleteUser(id string) (err error) { log.Printf("INFO [DeleteUser] connecting firestore start. userId=%v", id) // init firestore client ctx := context.Background() client, err := initFireStoreClient(ctx) defer client.Close() if err != nil { log.Printf("ERROR firestore clientの初期化に失敗 err=%v", err) return define.SYSTEM_ERR } _, err = client.Collection("users").Doc(id).Delete(ctx) if err != nil { log.Printf("ERROR firestoreのusersコレクションの削除に失敗 id=%s, err=%v", id, err) return define.FAILED_DELETE_USER } log.Printf("INFO [DeleteUser] connecting firestore end.") return nil }
6. API サーバ起動と動作確認
すこし説明を抜粋しすぎましたが詳細はgitを参照していただければと思います。 本節では作成したAPIを動かしてみます。
main.goにこれまで述べてきたcontroller層、application層、infrastructure層の依存を作成し、portを指定してリクエストを受け付けるサーバを実装します。
main.go
https://github.com/MoriTomo7315/go-user-rest-api/blob/master/main.go
package main import ( "fmt" "log" "net/http" "os" "github.com/MoriTomo7315/go-user-rest-api/application" "github.com/MoriTomo7315/go-user-rest-api/controller" "github.com/MoriTomo7315/go-user-rest-api/infrastructure/persistence" "github.com/joho/godotenv" ) func main() { //log設定 log.SetFlags(log.LstdFlags | log.Lmicroseconds | log.Llongfile) // envファイル読み込み _ = godotenv.Load(fmt.Sprintf("./.env.%s", os.Getenv("GO_ENV"))) // Start listening port server := http.Server{ Addr: ":50001", } /* DDD依存関係を定義 Infrastructure → Application → Controller */ // firestore用infrastructure firestoreClient := persistence.NewFirestoreClient() userApplication := application.NewUserApplication(firestoreClient) userController := controller.NewUserController(userApplication) //サーバーにController(ハンドラ)を登録 log.Printf("/api/users start") http.HandleFunc("/api/users", userController.HandlerHttpRequest) http.HandleFunc("/api/users/", userController.HandlerHttpRequestWithParameter) server.ListenAndServe() }
API サーバ起動コマンド
$ cd <your_path>/go-user-rest-api $ GO_ENV=local go run main.go 2022/02/10 21:20:53.080729 /Users/moritomokana/development/go_study/go-user-rest-api/main.go:36: /api/users start
7. 動作確認
今回はport 50001で動かしているのでhttp://localhost:50001に対して各endpointの動作確認をします。
GET /api/users
$ curl localhost:50001/api/users | jq . % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 667 100 667 0 0 973 0 --:--:-- --:--:-- --:--:-- 973 { "status": 200, "message": "ユーザ情報取得に成功しました。", "user_count": 2, "users": [ { "id": "CRe4XPnE8DjD4bjk7n2Y", "name": "testuser6", "prefecture": "神奈川県", "createdAt": "2022-01-29T13:52:38.936004Z", "updatedAt": "" }, { "id": "oaAwBZMSdmEC4OEbr99B", "name": "testuser5", "prefecture": "東京都", "createdAt": "2022-01-29T12:00:00Z", "updatedAt": "2022-01-29T13:50:59.146083Z" } ] } ~
GET /api/users/:user_id
$ curl localhost:50001/api/users/oaAwBZMSdmEC4OEbr99B | jq . % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 248 100 248 0 0 224 0 0:00:01 0:00:01 --:--:-- 224 { "status": 200, "message": "ユーザ情報取得に成功しました。", "user_count": 1, "users": [ { "id": "oaAwBZMSdmEC4OEbr99B", "name": "testuser5", "prefecture": "東京都", "createdAt": "2022-01-29T12:00:00Z", "updatedAt": "2022-01-29T13:50:59.146083Z" } ] }
POST /api/users
$ curl -X POST -H "Content-Type: application/json" -d '{"name":"testuser7", "prefecture":"大阪府"}' localhost:50001/api/users | jq . % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 131 100 85 100 46 38 20 0:00:02 0:00:02 --:--:-- 59 { "status": 201, "message": "作成に成功しました。", "user_count": 0, "users": null }
念の為、testuser7が作成されているか確認。
$ curl localhost:50001/api/users | jq . % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 798 100 798 0 0 3179 0 --:--:-- --:--:-- --:--:-- 3179 { "status": 200, "message": "ユーザ情報取得に成功しました。", "user_count": 3, "users": [ { "id": "CRe4XPnE8DjD4bjk7n2Y", "name": "testuser6", "prefecture": "神奈川県", "createdAt": "2022-01-29T13:52:38.936004Z", "updatedAt": "" }, { "id": "oaAwBZMSdmEC4OEbr99B", "name": "testuser5", "prefecture": "東京都", "createdAt": "2022-01-29T12:00:00Z", "updatedAt": "2022-01-29T13:50:59.146083Z" }, { "id": "uCcJCLGDxwmpmjIfZdUk", "name": "testuser7", "prefecture": "大阪府", "createdAt": "2022-02-10T12:36:27.945981Z", "updatedAt": "" } ] }
POST /api/users/:user_id
testuser7のprefectureを大阪府から京都府に変更してみる。
curl -X POST -H "Content-Type: application/json" -d '{"name": "testuser7", "prefecture":"京都府", "createdAt":"2022-02-10T12:36:27.945981Z"}' localhost:50001/api/users/uCcJCLGDxwmpmjIfZdUk | jq . % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 175 100 85 100 90 38 40 0:00:02 0:00:02 --:--:-- 79 { "status": 204, "message": "更新に成功しました。", "user_count": 0, "users": null }
更新されたかどうか確認。
$ curl localhost:50001/api/users/uCcJCLGDxwmpmjIfZdUk | jq . % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 254 100 254 0 0 261 0 --:--:-- --:--:-- --:--:-- 261 { "status": 200, "message": "ユーザ情報取得に成功しました。", "user_count": 1, "users": [ { "id": "uCcJCLGDxwmpmjIfZdUk", "name": "testuser7", "prefecture": "京都府", "createdAt": "2022-02-10T12:36:27.945981Z", "updatedAt": "2022-02-10T12:41:18.51411Z" } ] }
DELETE /api/users/:user_id
testuser7 (uid: uCcJCLGDxwmpmjIfZdUk)を削除してみる。
$ curl -X DELETE localhost:50001/api/users/uCcJCLGDxwmpmjIfZdUk | jq . % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 85 100 85 0 0 33 0 0:00:02 0:00:02 --:--:-- 33 { "status": 204, "message": "削除に成功しました。", "user_count": 0, "users": null }
testuser7が消えた確認。
$ curl localhost:50001/api/users | jq . % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 667 100 667 0 0 699 0 --:--:-- --:--:-- --:--:-- 699 { "status": 200, "message": "ユーザ情報取得に成功しました。", "user_count": 2, "users": [ { "id": "CRe4XPnE8DjD4bjk7n2Y", "name": "testuser6", "prefecture": "神奈川県", "createdAt": "2022-01-29T13:52:38.936004Z", "updatedAt": "" }, { "id": "oaAwBZMSdmEC4OEbr99B", "name": "testuser5", "prefecture": "東京都", "createdAt": "2022-01-29T12:00:00Z", "updatedAt": "2022-01-29T13:50:59.146083Z" } ] }
8. まとめ
- GinなどのREST APIフレームワークを使用せずに簡単なAPIを作成してみた。
- DBはfirestoreを使用した。credential情報のjsonはgithubなどのpublicに後悔しないように注意。
- DDDレイヤードアーキテクチャっぽい作りにしてみた。
今後の課題
- User更新は画面からリクエストされる想定なので、全項目値を受け取るように簡単に実装したが、変更点のみのものを受け取るように実装してもいいかもしれない。
- エラーハンドリングをもっと学ぶ
- Testちゃんと書く
- Cloud Runなどにデプロイしてみる。