TIGの伊藤真彦です。
渋川さんが投稿された
やりたいこと
私の最近の仕事はgo-swaggerによるバックエンドAPI開発です。本流はバックエンドですが、必要に応じてクラウドインフラを弄ったり、ちょっとしたフロントエンドアプリケーションを作ったりといった動き方で働いています。
ある時、go-swaggerで作ったバックエンドAPIの資産を使って、ちょっとした開発者向けアプリケーションを作りたくなりました。
ローカル環境でサーバーとフロントエンドアプリケーションを両方起動すると、サーバーがlocalhost:3000
フロントエンドがlocalhost:8080
を占拠してしまいます。また、フロントエンドとバックエンドのポートが異なることにより、フロントエンドからのリクエストを処理するためにはCORSの設定が必要になってしまいます。そして単純に2つのアプリケーションを起動するのが面倒だなと感じました。
そこで、go-swaggerが生成するものをハックして、フロントエンドの成果物と今まで作ってきたバックエンドAPIを同じポートで抱えつつ、APIを叩くクライアントサイドのコードもswagger.yamlから自動生成するようなアプリケーション開発に挑戦してみました。
バックエンド開発 説明のために、まずはバックエンドの資産を作ります。
詳しい作り方、説明はgo-swaggerでhello world をお読みください。
今回はこのようなディレクトリ構成でアプリケーションを作ります。
swagger.yaml server ├─gen └─get_greeting_handler.go
swagger.yaml --- swagger: '2.0' info: version: 1.0 .0 title: Greeting Server paths: /hello: get: produces: - text/plain parameters: - name: name required: false type: string in: query description: defaults to World if not given operationId: getGreeting responses: 200: description: returns a greeting schema: type: string description: contains the actual greeting as plain text
上記記事 と同じswagger.yamlを用意して、serverパッケージを生成します。
swagger generate server -a factory -A factory -t server/gen -f ./ swagger.yaml
get_greeting_handler.goはログ出力だけ少し追加しました。
get_greeting_handler.go func GetGreeting (p factory.GetGreetingParams) middleware.Responder { payload := "hello go" if p.Name != nil { payload = *p.Name } log.Printf("GetGreeting is called, return %s\n" , payload) return factory.NewGetGreetingOK().WithPayload(payload) }
localhost:3000/hello
でapiが動くことを確認します。
cd servergo run .\gen\cmd \factory-server\main.go --host 0 .0 .0 .0 --port 3000
フロンエンド開発 バックエンドの用意ができたら、上記のAPIを叩くためのサンプルアプリケーションを作ります。 まずはVue.jsでのHello Worldアプリケーションを生成します。 こちらも過去記事Electronの使い方 Web開発の技術でデスクトップアプリを作ろう で詳しく説明しています。
フロントエンドアプリケーションを下記の構成で生成します。
swagger.yaml app └─frontend server ├─gen └─get_greeting_handler.go
npm install -g @vue/cli mkdir appcd appvue create frontend
アプリケーションを起動し、localhost://8080
でフロントエンドアプリケーションが起動することを確認します。
フロントエンドにAPI Clientを実装する 作成したフロントエンドアプリケーション向けに、TypeScriptのAPIクライアントを自動生成します。
cd appnpx -p @openapitools/openapi-generator-cli@cli-4.1.3 openapi-generator generate -g typescript-axios -i ../../swagger.yaml -o ./frontend/src/client-axios -p modelPropertyNaming=snake_case --enable-post-process-file
/frontend/src/client-axios
ディレクトリにコードが生成されます。.eslintignore
に追加するなど、linterの設定を適宜追加してエラーが起きないようにすることを推奨します。
生成したコードを利用するようにapp\frontend\src\App.vue
を更新します。
App.vue <template > <div id ="app" > <img alt ="Vue logo" src ="./assets/logo.png" > <h1 > {{message}}</h1 > </div > </template > <script lang ="ts" > import { Component , Vue } from 'vue-property-decorator' ;import { DefaultApi } from './client-axios' @Component ({}) export default class App extends Vue { message = "" mounted ():void { const api = new DefaultApi ().getGreeting ("hello Vue + Go + OpenAPI" ); api.then ((resp: any ) => { this .message = resp.data ; }) } } </script > <style > #app { font-family : Avenir, Helvetica, Arial, sans-serif; -webkit-font-smoothing : antialiased; -moz-osx-font-smoothing : grayscale; text-align : center; color : #2c3e50 ; margin-top : 60px ; } </style >
app\frontend\src\client-axios\base.ts
でAPIの接続先を設定できるので、任意のポート、パスに書き換えます。
このままnpm run serve
でアプリケーションを起動すると、存在しないAPIにアクセスし、通信に失敗する状態になります。
この状態のアプリケーションを、バックエンドAPIと繋ぎこみます。
フロントエンドとバックエンドAPIを統合する まずは作成したフロントエンドアプリケーションをビルドし、アセットファイルを準備します。
cd app/frontendnpm run build
app\frontend\dist
ディレクトリに成果物一式が生成されます。
生成したファイルをgo:embed
で埋め込み起動するようなmain.go
を下記の構成で作成します。
swagger.yaml app ├─frontend └─main.go server ├─gen └─get_greeting_handler.go
main.go package mainimport ( "embed" "io/fs" "log" "net/http" "os" "github.com/go-openapi/loads" flags "github.com/jessevdk/go-flags" "regexp" "server/gen/restapi" "server/gen/restapi/factory" ) var proxyRegexp = regexp.MustCompile(`^/api` )var static embed.FSfunc main () { swaggerSpec, err := loads.Embedded(restapi.SwaggerJSON, restapi.FlatSwaggerJSON) if err != nil { log.Fatalln(err) } api := factory.NewFactoryAPI(swaggerSpec) server := restapi.NewServer(api) defer server.Shutdown() parser := flags.NewParser(server, flags.Default) parser.ShortDescription = "Greeting Server" parser.LongDescription = swaggerSpec.Spec().Info.Description server.ConfigureFlags() for _, optsGroup := range api.CommandLineOptionsGroups { _, err := parser.AddGroup(optsGroup.ShortDescription, optsGroup.LongDescription, optsGroup.Options) if err != nil { log.Fatalln(err) } } if _, err := parser.Parse(); err != nil { code := 1 if fe, ok := err.(*flags.Error); ok { if fe.Type == flags.ErrHelp { code = 0 } } os.Exit(code) } server.ConfigureAPI() http.HandleFunc("/api/" , func (w http.ResponseWriter, r *http.Request) { r.URL.Path = proxyRegexp.ReplaceAllString(r.URL.Path, "" ) server.GetHandler().ServeHTTP(w, r) }) public, err := fs.Sub(static, "frontend/dist" ) if err != nil { panic (err) } http.Handle("/" , http.FileServer(http.FS(public))) log.Println("listening on localhost:3000..." ) log.Fatal(http.ListenAndServe(":3000" , nil )) }
※そのまま使用するとserver
パッケージのimportに失敗します、ご自身の環境での適切なパスに指定するか、go.mod
でreplaceしてください” このファイルは、自動生成されたserver\gen\cmd\factory-server\main.go
をベースに拡張したファイルです。
生成したフロントエンドのコードをgo:embed
で埋め込みます。 詳しくはGo 1.16からリリースされたgo:embedとは をお読みください。
main.go
埋め込んだファイルを利用できるようにHTTPハンドラを設定します。
main.go public, err := fs.Sub(static, "frontend/dist" ) if err != nil { panic (err) } http.Handle("/" , http.FileServer(http.FS(public))
一方、go-swaggerで生成したバックエンドAPIのロジックはapi/hello
のパスでアクセスできるように退避させつつ読み込みます。
main.go server.ConfigureAPI() http.HandleFunc("/api/" , func (w http.ResponseWriter, r *http.Request) { r.URL.Path = proxyRegexp.ReplaceAllString(r.URL.Path, "" ) server.GetHandler().ServeHTTP(w, r) })
本来であればserver.Serve()
でポート番号の指定などを解釈しつつ、バックエンドAPIが起動するところを、上記の方法でうまく利用できました。
main.go var proxyRegexp = regexp.MustCompile(`^/api` )
パスにapi/
の文字列が存在するとバックエンドAPIが元々期待しているパスパターンと一致しないため、正規表現を用いて除外しています。 余談ですが正規表現のMustCompile
は関数内で行うと、呼び出されるたびに毎回コンパイルが走るため、グローバル変数に持たせる事が推奨されています。
アプリケーションの起動 これでアプリケーションが完成しました。 完成したアプリケーションを起動してみます。
無事にアプリケーションが起動し、バックエンドAPIからのレスポンスを表示できました。
ブラウザのデバッグコンソールでバックエンドAPIとの疎通に成功している事が確認できます。 あとは出来上がったファイルをgo build
すれば単一バイナリで動くフルスタックWebアプリケーションの完成です。
まとめ
go-swaggerで生成したバックエンドサーバーと、任意のフロントエンドWebアプリケーションを同じポートで起動することは可能。
クライアントサイドからバックエンドに繋ぐためのAPIクライアントも自動生成できる。
go:embed
を利用することで、単一バイナリとしてビルドすることが可能。
大規模アプリケーションをこの構成で作成するには若干邪道な雰囲気を感じますが、手早くアプリケーションを開発したいGopherのみなさんにおススメの手法でした。