Vercel × Go で開発するときの Tips
はじめに
Vercel に Go が無料でデプロイできることを下記の記事にて紹介しました。
今回はそんな 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

Vercel は添付した図のようなビルドプロセスとなっています。
Vercel CLI にてストレージにアップロードを行い、リモート環境のコンテナにてビルド(デプロイ)が実行されます。
そのためアップロードするファイルを除外しておくことはビルド(デプロイ)時間を短縮するときに有効となります。
.vercelignore ファイルをルートに作成し除外したいファイルを記載することで可能となります。
.gitignore と使い方は同じです。
routes
パスルーティングは vercel.json にて 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
標準ライブラリの 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 ディレクトリ以下に配置することで外部からは参照させないことができます。
しかし、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 を使っていると管理がめんどくさそうと気づきました。
(うまいことやらないとほかプロジェクトで管理しているテーブルが消されそう ...... )
sqlc と sqldef の組み合わせがマイブームなのでどうしようかなって感じです。
今回実装したコードは以下に置いておきます。
Discussion