俺流goのAPIサーバー
私流のAPIです。
誰かの参考になればうれしいです。
djangoから入ったためviewという言葉を使いがちですが、controllerだと思ってください。
middleware等を連鎖的に記述できるハンドラー関数。
道中でエラーがreturnされるとそのエラーがレスポンスに、エラーなく終えれば最後まで処理を実行します。
package handler
import "net/http"
func Handle(handlers ...func(w http.ResponseWriter, r *http.Request) error) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
for _, handler := range handlers {
if err := handler(w, r); err != nil {
return
}
}
}
}
例えばhttp.HandleFunc内で以下のように使います。
http.HandleFunc("/blog/post/", handler.Handle(middleware.PostOnlyMiddleware, blog.InsertBlogView))
PostOnlyMiddlewareでエラーを返すとそのエラーがレスポンスに、エラーがない場合blog.InsertBlogViewが実行されます。
レスポンスボディをjsonにする関数
どんな連想配列でも受け入れてしまう関数です。
http.ResponseWriterのWriteメソッドをbyte文字列のjsonにします。
package api
import (
"encoding/json"
"net/http"
)
func JsonResponse(w http.ResponseWriter, dictionary map[string]interface{}) bool {
data, err := json.Marshal(dictionary)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return false
}
w.WriteHeader(http.StatusOK)
w.Write(data)
return true
}
実際に使ってみる
package sample
import (
"errors"
"go_api/api"
"net/http"
)
var euro = map[string]string{
"2008": "Spain",
"2012": "Spain",
"2016": "Portugal",
}
func SampleView(w http.ResponseWriter, r *http.Request) error {
query := r.URL.Query()
year := query.Get("year")
if winner, ok := euro[year]; ok {
api.JsonResponse(w, map[string]interface{}{"winner": winner})
return nil
} else {
w.WriteHeader(http.StatusNotFound)
return errors.New("Not Found")
}
}
package main
import (
"go_api/handler"
"go_api/sample"
"net/http"
)
func main() {
http.HandleFunc("/", handler.Handle(sample.SampleView))
http.ListenAndServe(":8080", nil)
}
{"winner":"Portugal"}
となるはずです。
404 Not Found
となります。
Middlewareを定義していい感じにする
自分の場合フロントエンドはNextjsを使用しています。
オリジンが違うのでCORSの設定が必要です。
package middleware
import (
"net/http"
)
func CorsMiddleware(w http.ResponseWriter) error {
protocol := "http://"
host := "localhost:3000"
// こんな感じでローカルかどうか分岐
// if tools.IsProductionEnv() {
// protocol = "https://"
// host = os.Getenv("FRONT_HOST")
// }
w.Header().Set("Access-Control-Allow-Origin", protocol+host)
w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, X-Requested-With, Origin, X-Csrftoken, Accept, Cookie")
w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PUT")
return nil
}
メソッドを限定したいときもあるでしょう。
package middleware
import (
"errors"
"net/http"
)
// preflightに対してstatus okを返す
// そうしないとPUTやDELETEのような非単純リクエストが実行されない
func AllowOptionsMiddleware(w http.ResponseWriter, r *http.Request) error {
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return nil
}
return nil
}
func PostOnlyMiddleware(w http.ResponseWriter, r *http.Request) error {
if r.Method == "POST" {
return nil
}
w.WriteHeader(http.StatusMethodNotAllowed)
return errors.New("METHOD NOT ALLOWED")
}
func GetOnlyMiddleware(w http.ResponseWriter, r *http.Request) error {
if r.Method == "GET" {
return nil
}
w.WriteHeader(http.StatusMethodNotAllowed)
return errors.New("METHOD NOT ALLOWED")
}
今回の例でこれを使うと以下のようになります。
まず、corsとpreflightに関しては全ルート共通なので、handlerに入れてしまいます。
package handler
import "net/http"
func Handle(handlers ...func(w http.ResponseWriter, r *http.Request) error) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
middleware.CorsMiddleware(w)
middleware.AllowOptionsMiddleware(w, r)
for _, handler := range handlers {
if err := handler(w, r); err != nil {
return
}
}
}
}
このようにすると全ページに対してCorsの設定が有効になり、preflightに対してもstatusOKを返してくれます。
共通でないmiddlewareは以下のように使います。
GETメソッドのみ許可したい場合、main.goを変更します。
package main
import (
"go_api/handler"
"go_api/middleware"
"go_api/sample"
"net/http"
)
func main() {
http.HandleFunc("/", handler.Handle(middleware.GetOnlyMiddleware, sample.SampleView))
http.ListenAndServe(":8080", nil)
}
こうすることで、GET以外のメソッドに対して405エラーを返してくれます。
終わりに
最近クリーンアーキテクチャを学んでいます。
クリーンアーキテクチャを踏襲しつつ、さらにinterfaceを使ったもっといい感じのができたらそちらも投稿しようと思っています。
今回のものはこちらにまとめてあります。
Discussion