🐁

Vercel × Go で開発するときの Tips

2024/07/15に公開

はじめに

Vercel に Go が無料でデプロイできることを下記の記事にて紹介しました。

https://zenn.dev/otakakot/articles/9e9269a87aafeb

今回はそんな Vercel × Go における開発 Tips についてツラツラとご紹介します。

version

go version
go version go1.22.5 darwin/arm64
vercel --version
Vercel CLI 34.3.1
34.3.1

build and ignore

https://vercel.com/docs/deployments/builds

Vercel は添付した図のようなビルドプロセスとなっています。
Vercel CLI にてストレージにアップロードを行い、リモート環境のコンテナにてビルド(デプロイ)が実行されます。
そのためアップロードするファイルを除外しておくことはビルド(デプロイ)時間を短縮するときに有効となります。
.vercelignore ファイルをルートに作成し除外したいファイルを記載することで可能となります。

https://vercel.com/docs/deployments/vercel-ignore

.gitignore と使い方は同じです。

routes

パスルーティングは vercel.json にて routes を設定することで可能となります。

https://vercel.com/docs/projects/project-configuration#routes

ルーティング

{
    "routes": [
        {
            "src": "/",
            "dest": "/api",
            "methods": [
                "GET"
            ]
        }
    ]
}

上記記述によって

  • ルートパス / に対してディレクトリ /api に配置した index.go を設定
    index.go
    package api
    
    import (
    	"fmt"
    	"net/http"
    )
    
    func Handler(w http.ResponseWriter, r *http.Request) {
    	fmt.Fprintf(w, "<h1>Hello from Go!</h1>")
    }
    
  • method として GET のみを許可

が設定されます。
以下実際にアクセスした結果です。

curl -X GET -I https://<project>.vercel.app/ 
HTTP/2 200
# ...
curl -X POST -I https://<project>.vercel.app/ 
HTTP/2 404
# ...

POST でアクセスした場合は 405 Method Not Allowed ではなく 404 Not Found が返ってくることに注意が必要です。

パスパラメータ

vercel.json を以下のように記載するとパスパラメータ込みでルーティングすることが可能になります。

{
    "routes": [
        {
            "src": "/todos/(?<id>[^/]*)",
            "dest": "/api/todos/id",
            "methods": [
                "GET",
                "PUT",
                "DELETE"
            ]
        }
    ]
}

index.go では以下のようにしてパスパラメータを取得しています。
(無理矢理取得してます。もう少し良い方法はあると思います。)

index.go
func Handler(w http.ResponseWriter, r *http.Request) {
	id := strings.TrimPrefix(r.URL.Path, "/todos/")

    // ...
}

クエリパラメータ

vercel.json を以下のように記載するとクエリパラメータ込みでルーティングすることが可能になります。

{
    "routes": [
        {
            "src": "/todos(?<query>[^/]*)",
            "dest": "/api/tosos",
            "methods": [
                "GET"
            ]
        },
    ]
}

index.go では以下のようにしてクエリパラメータを取得しています。

index.go
func Handler(w http.ResponseWriter, r *http.Request) {
	value := r.URL.Query().Get("key") // key 値を指定して value を取得する

    // ...
}

test

https://pkg.go.dev/net/http/httptest

標準ライブラリの net/http/httptest を使えば Handler に対して直接テストが実行できます。
実装は以下のようなイメージになります。

index_test.go
package api_test

import (
	"bytes"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/google/uuid"

	api "github.com/otakakot/sample-go-vercel-tips/api/todos/id"
)
func TestHandler(t *testing.T) {
	t.Parallel()

	t.Run("GET_/todos/{id}", func(t *testing.T) {
		t.Parallel()

		w := httptest.NewRecorder()

		r := httptest.NewRequest(http.MethodGet, "/todos/"+uuid.NewString(), nil)

		api.Handler(w, r) // 処理実行

		t.Cleanup(func() {
			r.Body.Close()
		})

		// ステータスコードの比較
		if w.Code != http.StatusOK {
			t.Errorf("got: %v\nwant: %v", w.Code, http.StatusOK)
		}

		// レスポンスボディの検証
		got := api.GetResponse{}

		if err := json.NewDecoder(w.Body).Decode(&got); err != nil {
			t.Fatal(err)
		}

		// 比較処理 ...
	})
}

ステータスコードによってレスポンスボディが変わったりするのであえてテーブル駆動テストで書いていないです。

internal package

Go は internal ディレクトリ以下に配置することで外部からは参照させないことができます。

https://qiita.com/tenntenn/items/d8db61720a5ce7fbdeb6

しかし、Vercel では internal パッケージを使うと下記のようなログが出力されビルドエラーとなりデプロイができません。

package command-line-arguments
	imports api/api/todos
	index.go:10:2: use of internal package github.com/otakakot/<project>/internal/log not allowed
failed to `go build`
Error: Command failed: go build -ldflags -s -w -o /tmp/62080c8d/bootstrap /vercel/path0/main__vc__go__.go

原因はあまりわかっていません。解決策知っている方いらっしゃいましたら教えてください。

おまけ

api/index.go をルートとして設定して隣に openapi.yaml を配置して embed を使えば petstore から参照させたりなんかもできます。

index.go
package api

import (
	_ "embed"
	"net/http"
)

//go:embed openapi.yaml
var yaml []byte

func Handler(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Access-Control-Allow-Origin", "*")

	w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")

	w.Header().Set("Access-Control-Allow-Headers", "Content-Type")

	w.Header().Set("Content-Type", "application/yaml")

	w.Write(yaml)
}

あと、型が欲しいので oapi-codegen を自動生成のために使ってたりもしてます。

おわりに

無料版だと 1 アカウントあたり 1 Postgres となるのでマイグレーションツールに sqldef を使っていると管理がめんどくさそうと気づきました。
(うまいことやらないとほかプロジェクトで管理しているテーブルが消されそう ...... )
sqlcsqldef の組み合わせがマイブームなのでどうしようかなって感じです。

https://zenn.dev/otakakot/scraps/acad6c3d8758d8

今回実装したコードは以下に置いておきます。

https://github.com/otakakot/sample-go-vercel-tips

Discussion