LayerX エンジニアブログ

LayerX の エンジニアブログです。

freee API のGoクライアントライブラリを公開しました

DX事業部の @yyoshiki41(中川佳希)です。 現在は主に、LayerX インボイス という経理業務を行う方を対象ユーザーにした SaaS プロダクトを開発しています。

先日、会計freee API のGoクライアントを公開しました 🎉 github.com 今回はそのGo実装内部について紹介していきます。

会計ソフトとLayerX インボイス

LayerX インボイスは、以下のような業務をサポートするプロダクトになっています。

  1. 請求書の受領
  2. 請求書から仕訳を行う
  3. オンラインバンキングに取り込むためのデータを出力する
  4. 会計ソフトに仕訳データを連携する

LayerX インボイス上で扱うデータは最終的に会計ソフトに取り込まれます。 プロトコルや出力形式などは、会計ソフトにより異なります。

今回紹介する会計ソフトfreee の場合、Public API が公開されています。 freee アプリストアに登録した開発者の方であれば、自由に開発することが出来ます。ドキュメントも手厚く公開されており、デベロッパーフレンドリーさに非常に感謝しております 🙏

会計ソフトfreeeをお使いのLayerX インボイスユーザーの方にとって、API 連携は

  • 手作業がなくなる
  • 会計ソフトの連携履歴がシステム上に残る

などのメリットがあり、より良い体験を提供する上でなくてはならない機能です。連携機能のアップデートも日々、力を注いでいます。 layerx.co.jp

Go ライブラリの開発

会計freee API 用のSDKは、PHP, Java などで既に公開されているものがあります。 github.com

LayerX インボイスはGoで開発しているため、APIコールをラップするクライアントライブラリを実装することにしました。 会計freee API は OpenAPI v3 のスキーマファイルも公開されており、はじめは openapi-generator などの自動生成を用いようとしましたが、一部スキーマで生成コードをそのまま利用することが出来ない箇所があり、実装ロジックもほぼないためスクラッチで実装することにしました。

ライブラリの初期化

OAuth2 アプリの client_id, client_secret, redirect_url をセットすることで初期化します。

import (
    "log"
    "os"

    freee "github.com/LayerXcom/freee-go"
)

func NewClient(clientID, clientSecret, redirectURL string) *freee.Client {
    conf := freee.NewConfig(clientID, clientSecret, redirectURL)
    conf.Log = log.New(os.Stdout, "", log.LstdFlags)
    client := freee.NewClient(conf)
    return client
}

ライブラリ内部でのログ出力用のロガーもセットすることが出来ます。認証部分やAPIエラーではない処理エラーの出力や、freee API サーバーがHTTPレスポンスヘッダーに付与してくれる X-Freee-Request-ID というAPIリクエスト毎にユニークなIDをログ出力するなどに役立ちます。

OAuth2トークンはユーザーごとに異なるため初期化の際ではなく、API呼び出し時にセットする必要があります。(ここでのユーザーは、会計freee のユーザーを指します。)

OAuth2.0 実装

認証方式は、OAuth2.0 です。 ライブラリ内部では、golang.org/x/oauth2 を使用しています。 ライブラリユーザーが、セキュアにかつ、OAuth ダンスで考慮すべきことが少なくなるよう実装しています。

例えば、OAuth 2.0 プロバイダー(freee)の認可ページURLの取得や、認可コードからOAuth2トークンの払い出しなどを行う関数もクライアント内で提供しています。

package freee

import (
    "context"

    "golang.org/x/oauth2"
)

const (
    Oauth2TokenURL = "https://accounts.secure.freee.co.jp/public_api/token"
    Oauth2AuthURL  = "https://accounts.secure.freee.co.jp/public_api/authorize"
)

func (c *Client) AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string {
    return c.config.Oauth2.AuthCodeURL(state, opts...)
}

func (c *Client) Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
    return c.config.Oauth2.Exchange(ctx, code, opts...)
}

トークンのリフレッシュ

freee API のアクセストークンの有効期限は、発行から24時間になっています。リフレッシュトークンは無期限です。 ライブラリユーザーがリクエストの度に、アクセストークン有効期限の確認を行い、期限切れの場合リフレッシュを行うなどは非常に煩雑なため、避けたいはずです。

golang.org/x/oauth2oauth2.NewClient にて、リフレッシュを Transport 層で行うよう実装された http.Client を提供しています。 このクライアントを使ってリクエストすれば、トークンリフレッシュをライブラリユーザー側で気にせずともよくなります。 内部の実装を見ると、 oauth2.Token から有効なトークンか(Expiry: 有効期限を用いている)を判断してリクエストを行う RoundTripper を実装しています。

またトークンのリフレッシュ実装を type TokenSource のインターフェイスを実装することも出来ます。 例えば、golang.org/x/oauth2/google には Google Compute Engine の実行環境上からクレデンシャルを取得する実装が用意されています。 簡単な実装例は以下の記事でも紹介しています。 qiita.com

API コール例

freee-go からのレスポンスとして、 token が常に返されます。これは、API コール前の token が有効期限切れの場合にはリフレッシュされた token が返ってきます。 下記ではdefer などで必ずリフレッシュされた token を保存するように実装しておく例です。

import (
    "log"

    freee "github.com/LayerXcom/freee-go"
)

func main() {
    ...
    // データストアから、token を取得してくる
    token, err := retrieveYourTokenFromDataStore(ctx, token)
    // defer 内で、データストアへの保存を行う
    defer func() {
        if token.Valid() {
            saveYourTokenInDataStore(ctx, token)
        }
    }()

    // API Call
    me, token, err := client.GetUsersMe(ctx, token, freee.GetUsersMeOpts{})
    if err != nil {
        log.Fatal(err)
    }
    // リフレッシュされた token
    log.Printf("%v", token)
}

エラーハンドリング

API サーバーからのエラーか、access_token, refresh_token が共に revoke されており再度 OAuth認証が必要なエラーか、を切り分けてハンドリングすることは、エンドユーザーの体験に関わる重要な点です。 freee-go では、以下のような Error 構造体を定義してハンドリング出来るように実装しています。

type Error struct {
    StatusCode              int
    RawError                string
    IsAuthorizationRequired bool
}

例えば、ライブラリユーザー側で下記のようなチェックを行うことが出来ます。

   // API Call
    me, token, err := client.GetUsersMe(ctx, token, freee.GetUsersMeOpts{})
    if err != nil {
        if e, ok := err.(*freee.Error); ok {
            if e.IsAuthorizationRequired {
                // required re-authentication
            }
            // other errors
        }
        return err
    }

おわりに

LayerX では今後も自社で開発したものを公開して、エンドユーザーのみならずコミュニティなどへの貢献も進めていきます。 Github で公開されているレポジトリなども複数あります。興味が湧いたという方は、ぜひ一度話を聞きに来てください!

pkg.go.dev

https://herp.careers/v1/layerx/8hSahFbczwj7herp.careers