はじめに
今回はNextJSのサーバーアクションを実装する際に、
Supabaseのクエリ生成を共通関数内に実装したことで、
より好ましいコードとなったことを共有したいため記事にしました!
時は深夜、NextJSでユーザーを挿入するサーバーアクションを作成していたところ、
supabaseのimportが何度も現れていることに気付き、疑問が浮かびました。
問題:同じimportを何度も行うべきか?
// 全てのサーバーアクションでインポートするべきか?
import { supabase } from '@/utils/server/supabaseClient';
import type { User } from '@/customTypes/User';
import { handleSupabaseRequest } from '@/utils/server/handleSupabaseRequest';
export async function insertUser(userName: string): Promise<User> {
const data = await handleSupabaseRequest( // ここで使用する
supabase
.from('users')
.insert({ user_name: userName })
.select()
.single()
);
return data;
}
たかが1行のimport文ですが、
サーバーアクションが1兆個存在すれば1兆行に増えてしまうでしょう!
DRY(Don't Repeat Yourself)原則の信者である私は許すことができませんでした :(
さらに、supabaseへのアクセスは必ずsupabaseの共通関数を通すように実装したかったので、「共通関数内でこれを定義できないか?」と思いました。
そこで見つけた解決策が次のとおりです。
解決策:共通関数を使ったSupabaseクエリの管理
私が採用したのは、共通関数でsupabaseインスタンスを管理する方法です。
まず、元のsupabase共通関数は以下のようになっていました。
export async function handleSupabaseRequest<T>(supabaseQuery: Promise<{ data: T | null; error: any }>): Promise<T> {
const { data } = await supabaseQuery;
return data;
}
この共通関数はsupabaseQueryを引数として受け取るため、
呼び出し側でsupabaseをimportしてクエリを作成する必要があります1。
そこで、共通関数側でクエリを作成できるよう、以下のように変更しました。
import { supabase } from '@/utils/server/supabaseClient';
// クエリビルダー関数の型
type QueryBuilder<T> = (supabaseClient: typeof supabase) => Promise<{ data: T | null; error: any }>;
export async function handleSupabaseRequest<T>(queryBuilder: QueryBuilder<T>): Promise<T> {
const { data, error } = await queryBuilder(supabase);
return data as T;
}
supabaseの"クエリ作成関数"をコールバック関数とすることで、
共通関数内でsupabaseインスタンスを使い、クエリを生成できるようになりました。
import type { User } from '@/customTypes/User';
import { handleSupabaseRequest } from '@/utils/server/handleSupabaseRequest';
export async function insertUser(userName: string): Promise<User> {
const data = await handleSupabaseRequest(async (supabase) => {
return supabase
.from('users')
.insert({ user_name: userName })
.select()
.single();
});
return data;
}
これにより、呼び出し側でsupabaseのクエリを作成することなく、
コールバック関数からsupabaseのクエリビルダーを呼び出せるようになりました。
また、「supabaseのインスタンスをどこで呼び出しているのか?」と疑問に思った場合、
必然的にsupabase共通関数に行き着くため、全体像を理解しやすくなるのではと思います。
サーバーアクション側のコードも簡潔になり、エラーハンドリングも共通関数内で一度行えばよいため、今回のケースではより良いコードになりました。
ご一読いただきありがとうございました!
参考
-
依存性の注入の観点からは、これは好ましい状態ですが、今回の場合は全て同じsupabaseインスタンスを使用していたため、共通化を行うべきではないかと判断しました。 ↩