OpenAPI Generator + golang + Flutter でアプリ開発

1. はじめに

以下のような構成のスマホアプリを作ってみようと思います。

f:id:daigo-knowlbo:20181103213543p:plain

一般的な、HTTP経由でサーバと通信するタイプのスマホアプリです。

で、タイトルの通り「OpenAPI 3.0でWebAPIを定義」し「golangでWebAPIを実装」し「Flutterでスマホアプリを実装」する、ということをしたいと思います。
また、OpenAPI 3.0定義からサーバ及びクライアントコードを自動生成するために OpenAPI Generator を使用することとします。

github.com

今回利用した環境  

macOS Mojave 10.14
Flutter 0.8.2
go 1.10.3
openapi-generator 3.3.2

2. 準備

Flutter / go 環境は構築済みの前提とします。
openapi-generatorは以下のbrewコマンドでインストールします。

$ brew install openapi-generator

3. 作る!

大まかな流れは以下になります。

  • OpenAPI 3.0 specでAPI仕様を定義
  • OpenAPI Generatorでgolangのserverコードを自動生成
  • OpenAPI Generatorでdartのclientコードを自動生成
  • Flutterアプリを作成(dart clientを利用してgolang serverを呼び出す)

では順番に進めていこうと思います。
まずは、アプリ開発用のディレクトリを作成します。

$ mkdir oas3_go_flutter_exam
$ cd oas3_go_flutter_exam

3.1. OpenAPI 3.0 specを作成

OpenAPI定義yamlを作成します。

ファイル名は「employee_api.yml」とします。
idを指定してemployee情報を取得するWebAPIの定義になります。
Employeeは id / firstName / lastName / salary の属性を持ったオブジェクトとします。

openapi: "3.0.0"
info:
  version: 1.0.0
  title: oas3_go_flutter_exam Employee
  license:
    name: MIT
servers:
  - url: http://localhost:8080/api/
paths:
  /employee:
    get:
      summary: get employee information
      operationId: getEmployee
      tags:
        - employee
      parameters:
        - name: employeeId
          in: query
          description: query target employee id
          required: true
          schema:
            type: string
      responses:
        '200':
          description: return employee information
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Employee"
components:
  schemas:
    Employee:
      required:
        - id
        - firstName
        - lastName
        - salary
      properties:
        id:
          type: string
        firstName:
          type: string
        lastName:
          type: string
        salary:
          type: integer
          format: int64

3.2. yamlの妥当性をチェック

定義した employee_api.yml にエラーがないか、以下のコマンドでチェックします。

$ openapi-generator validate -i employee_api.yml

Validating spec (employee_api.yml)
No validation issues detected.

3.3. golangのserverコードを自動生成

以下のコマンドでgolangのserverコードを自動生成します。

$ openapi-generator generate -i employee_api.yml -g go-server -o ./server

[main] WARN  o.o.c.ignore.CodegenIgnoreProcessor - Output directory does not exist, or is inaccessible. No file (.openapi-generator-ignore) will be evaluated.
[main] INFO  o.o.c.languages.AbstractGoCodegen - Environment variable GO_POST_PROCESS_FILE not defined so Go code may not be properly formatted. To define it, try `export GO_POST_PROCESS_FILE="/usr/local/bin/gofmt -w"` (Linux/Mac)
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./server/go/model_employee.go
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./server/go/api_employee.go
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./server/api/openapi.yaml
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./server/main.go
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./server/Dockerfile
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./server/go/routers.go
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./server/go/logger.go
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./server/go/README.md
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./server/.openapi-generator-ignore
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./server/.openapi-generator/VERSION

モデルクラス(model_employee.go)やルート定義(routers.go)、APIのカラ定義(api_employee.go)など一通りのgo実装が自動生成されました。

3.4. goのWebAPIを実装

自動生成されたコードにオリジナルの実装を追加します。

api_employee.goファイル の GetEmployee() に Employeeオブジェクト を返却する実装を以下のように追加します。
(本来はDBアクセスなどを行った結果を返すと思います)

package openapi

import (
    "encoding/json"
    "net/http"
)

// GetEmployee - get employee information
func GetEmployee(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json; charset=UTF-8")
    w.WriteHeader(http.StatusOK)

    employeeID := r.URL.Query()["employeeId"]

    employee := Employee{
        Id:        employeeID[0],
        FirstName: "ryuichi " + employeeID[0],
        LastName:  "daigo " + employeeID[0],
        Salary:    12000000}
    json.NewEncoder(w).Encode(employee)
}

3.5. go serverを実行

WebAPIがうまく動作するか実行してみましょう。

$ go run main.go

curlでもブラウザでも何でも良いですが、以下のURLにGETを投げてみます。

http://localhost:8080/api/employee?employeeId=10

{"id":"10","firstName":"ryuichi 10","lastName":"daigo 10","salary":12000000}

golang serverのWebAPIが正しく動作していることを確認できました。

3.6. dartのclientコードを自動生成

以下のコマンドでdartのclientコードを自動生成します。

$ openapi-generator generate -i employee_api.yml -g dart -DbrowserClient=false -o ./client

[main] WARN  o.o.c.ignore.CodegenIgnoreProcessor - Output directory does not exist, or is inaccessible. No file (.openapi-generator-ignore) will be evaluated.
[main] INFO  o.o.c.languages.DartClientCodegen - Environment variable DART_POST_PROCESS_FILE not defined so the Dart code may not be properly formatted. To define it, try `export DART_POST_PROCESS_FILE="/usr/local/bin/dartfmt -w"` (Linux/Mac)
[main] INFO  o.o.c.languages.DartClientCodegen - Dart version: 2.x
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client/lib/model/employee.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client/docs/Employee.md
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client/lib/api/employee_api.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client/docs/EmployeeApi.md
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client/pubspec.yaml
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client//lib/api_client.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client//lib/api_exception.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client//lib/api_helper.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client//lib/api.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client//lib/auth/authentication.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client//lib/auth/http_basic_auth.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client//lib/auth/api_key_auth.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client//lib/auth/oauth.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client/git_push.sh
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client/.gitignore
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client/README.md
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client/.openapi-generator-ignore
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client/.openapi-generator/VERSION

※ 上記のように「-DbrowserClient=false」オプションを付けないとFlutterでコンパイルできない dart:html に依存したコードが生成されてしまうので注意。

dartによりWebAPI呼び出しを行うコード、また送受信するデータのモデルクラスなど一通りの実装が自動生成されました。

3.7. Flutterアプリを作成

以下のコマンドでFlutterアプリを作成します。

 $ flutter create flutter_app

先程自動生成した dart client を利用して golang server にアクセスするために pubspec.yamldart client への依存定義を追加します。

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^0.1.2

  openapi:
    path: ../client/

※ 上記定義の下2行

main.dartを修正します。
IDを入力し、ボタンを押すとWebAPI呼び出しが行われ、結果を画面にテキスト表示するようにします。

// main.dart

import 'package:flutter/material.dart';
import 'package:openapi/api.dart';

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demo',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new MyHomePage(title: 'Flutter with OpenAPI Generator'),
    );
  }
}
 
class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => new _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {

  String _employeeName = '';
  String _employeeId = '';

  void _callWebApi() {    
    var client = new EmployeeApi();
    var result = client.getEmployee(this._employeeId);
    result.then(
      (employee) => setState(() { this._employeeName = employee.firstName + ' ' + employee.lastName; } )
    );
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text(widget.title),
      ),
      body: new Center(
        child: new Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            new Text('pleas input employee id'),
            new TextField(
              onChanged: (v) => this._employeeId = v,
            ),
            new RaisedButton(
              child: new Text('call WebAPI'),
              onPressed: _callWebApi,
            ),
            new Text(
              this._employeeName,
              style: Theme.of(context).textTheme.display1,
            ),
          ],
        ),
      ),
    );
  }
}

3.8. 実行

では実行してみます。

./serverディレクトリで以下のコマンドによりgolang serverを起動しておきます。

go run main.go

そしてFlutterアプリを起動します。

f:id:daigo-knowlbo:20181103211906p:plain

IDを入力し、call WebAPIボタンをタップします。

f:id:daigo-knowlbo:20181103212648p:plain

WebAPI呼び出しが行われEmployeeオブジェクトの取得&表示が行われました!

4. まとめ

OpenAPI Generator を使うとモデルを含めたテンプレート実装が一瞬のうちに生成されかなりいい感じに開発が進められるんじゃないかと思いました。サクサクっと必要な定形コードが自動生成されるので、ドメイン領域への集中力アップにも勿論効果的ですね。
OpenAPI 3.0によるAPI定義を明確に行った上で、サービスを実装していくスタイルも(まあSwagger時代から何年も行われていることではありますが)クリーンにプロジェクトが保たれていいと思います。

今回作ったコードは以下に置いておきました。

github.com